mirror of
https://github.com/kmc7468/arkvault.git
synced 2026-02-04 16:16:55 +00:00
Compare commits
5 Commits
328baba395
...
v0.5.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0cd55a413d | ||
|
|
361d966a59 | ||
|
|
aef43b8bfa | ||
|
|
7f128cccf6 | ||
|
|
a198e5f6dc |
@@ -2,7 +2,11 @@
|
|||||||
FROM node:22-alpine AS base
|
FROM node:22-alpine AS base
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
RUN npm install -g pnpm@10
|
RUN apk add --no-cache bash curl && \
|
||||||
|
curl -o /usr/local/bin/wait-for-it https://raw.githubusercontent.com/vishnubob/wait-for-it/master/wait-for-it.sh && \
|
||||||
|
chmod +x /usr/local/bin/wait-for-it
|
||||||
|
|
||||||
|
RUN npm install -g pnpm@9
|
||||||
COPY pnpm-lock.yaml .
|
COPY pnpm-lock.yaml .
|
||||||
|
|
||||||
# Build Stage
|
# Build Stage
|
||||||
@@ -25,4 +29,4 @@ COPY --from=build /app/build ./build
|
|||||||
|
|
||||||
EXPOSE 3000
|
EXPOSE 3000
|
||||||
ENV BODY_SIZE_LIMIT=Infinity
|
ENV BODY_SIZE_LIMIT=Infinity
|
||||||
CMD ["node", "./build/index.js"]
|
CMD ["bash", "-c", "wait-for-it ${DATABASE_HOST:-localhost}:${DATABASE_PORT:-5432} -- node ./build/index.js"]
|
||||||
|
|||||||
@@ -3,8 +3,7 @@ services:
|
|||||||
build: .
|
build: .
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
depends_on:
|
depends_on:
|
||||||
database:
|
- database
|
||||||
condition: service_healthy
|
|
||||||
user: ${CONTAINER_UID:-0}:${CONTAINER_GID:-0}
|
user: ${CONTAINER_UID:-0}:${CONTAINER_GID:-0}
|
||||||
volumes:
|
volumes:
|
||||||
- ./data/library:/app/data/library
|
- ./data/library:/app/data/library
|
||||||
@@ -36,8 +35,3 @@ services:
|
|||||||
environment:
|
environment:
|
||||||
- POSTGRES_USER=arkvault
|
- POSTGRES_USER=arkvault
|
||||||
- POSTGRES_PASSWORD=${DATABASE_PASSWORD:?}
|
- POSTGRES_PASSWORD=${DATABASE_PASSWORD:?}
|
||||||
healthcheck:
|
|
||||||
test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER}"]
|
|
||||||
interval: 5s
|
|
||||||
timeout: 5s
|
|
||||||
retries: 5
|
|
||||||
|
|||||||
66
package.json
66
package.json
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "arkvault",
|
"name": "arkvault",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.5.1",
|
"version": "0.5.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite dev",
|
"dev": "vite dev",
|
||||||
@@ -16,53 +16,53 @@
|
|||||||
"db:migrate": "kysely migrate"
|
"db:migrate": "kysely migrate"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/compat": "^1.4.1",
|
"@eslint/compat": "^1.3.1",
|
||||||
"@iconify-json/material-symbols": "^1.2.44",
|
"@iconify-json/material-symbols": "^1.2.29",
|
||||||
"@sveltejs/adapter-node": "^5.4.0",
|
"@sveltejs/adapter-node": "^5.2.13",
|
||||||
"@sveltejs/kit": "^2.48.4",
|
"@sveltejs/kit": "^2.22.5",
|
||||||
"@sveltejs/vite-plugin-svelte": "^6.2.1",
|
"@sveltejs/vite-plugin-svelte": "^4.0.4",
|
||||||
"@types/file-saver": "^2.0.7",
|
"@types/file-saver": "^2.0.7",
|
||||||
"@types/ms": "^2.1.0",
|
"@types/ms": "^0.7.34",
|
||||||
"@types/node-schedule": "^2.1.8",
|
"@types/node-schedule": "^2.1.8",
|
||||||
"@types/pg": "^8.15.6",
|
"@types/pg": "^8.15.4",
|
||||||
"autoprefixer": "^10.4.21",
|
"autoprefixer": "^10.4.21",
|
||||||
"axios": "^1.13.1",
|
"axios": "^1.10.0",
|
||||||
"dexie": "^4.2.1",
|
"dexie": "^4.0.11",
|
||||||
"eslint": "^9.39.0",
|
"eslint": "^9.30.1",
|
||||||
"eslint-config-prettier": "^10.1.8",
|
"eslint-config-prettier": "^10.1.5",
|
||||||
"eslint-plugin-svelte": "^3.13.0",
|
"eslint-plugin-svelte": "^3.10.1",
|
||||||
"eslint-plugin-tailwindcss": "^3.18.2",
|
"eslint-plugin-tailwindcss": "^3.18.0",
|
||||||
"exifreader": "^4.32.0",
|
"exifreader": "^4.31.1",
|
||||||
"file-saver": "^2.0.5",
|
"file-saver": "^2.0.5",
|
||||||
"globals": "^16.5.0",
|
"globals": "^16.3.0",
|
||||||
"heic2any": "^0.0.4",
|
"heic2any": "^0.0.4",
|
||||||
"kysely-ctl": "^0.19.0",
|
"kysely-ctl": "^0.13.1",
|
||||||
"lru-cache": "^11.2.2",
|
"lru-cache": "^11.1.0",
|
||||||
"mime": "^4.1.0",
|
"mime": "^4.0.7",
|
||||||
"p-limit": "^7.2.0",
|
"p-limit": "^6.2.0",
|
||||||
"prettier": "^3.6.2",
|
"prettier": "^3.6.2",
|
||||||
"prettier-plugin-svelte": "^3.4.0",
|
"prettier-plugin-svelte": "^3.4.0",
|
||||||
"prettier-plugin-tailwindcss": "^0.7.1",
|
"prettier-plugin-tailwindcss": "^0.6.14",
|
||||||
"svelte": "^5.43.2",
|
"svelte": "^5.35.6",
|
||||||
"svelte-check": "^4.3.3",
|
"svelte-check": "^4.2.2",
|
||||||
"tailwindcss": "^3.4.18",
|
"tailwindcss": "^3.4.17",
|
||||||
"typescript": "^5.9.3",
|
"typescript": "^5.8.3",
|
||||||
"typescript-eslint": "^8.46.2",
|
"typescript-eslint": "^8.36.0",
|
||||||
"unplugin-icons": "^22.5.0",
|
"unplugin-icons": "^22.1.0",
|
||||||
"vite": "^7.1.12"
|
"vite": "^5.4.19"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fastify/busboy": "^3.2.0",
|
"@fastify/busboy": "^3.1.1",
|
||||||
"argon2": "^0.44.0",
|
"argon2": "^0.43.0",
|
||||||
"kysely": "^0.28.8",
|
"kysely": "^0.28.2",
|
||||||
"ms": "^2.1.3",
|
"ms": "^2.1.3",
|
||||||
"node-schedule": "^2.1.1",
|
"node-schedule": "^2.1.1",
|
||||||
"pg": "^8.16.3",
|
"pg": "^8.16.3",
|
||||||
"uuid": "^13.0.0",
|
"uuid": "^11.1.0",
|
||||||
"zod": "^3.25.76"
|
"zod": "^3.25.76"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^22.0.0",
|
"node": "^22.0.0",
|
||||||
"pnpm": "^10.0.0"
|
"pnpm": "^9.0.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
1790
pnpm-lock.yaml
generated
1790
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -32,7 +32,7 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
if ($info) {
|
if ($info?.dataKey) {
|
||||||
requestFileThumbnailDownload($info.id, $info.dataKey)
|
requestFileThumbnailDownload($info.id, $info.dataKey)
|
||||||
.then((thumbnailUrl) => {
|
.then((thumbnailUrl) => {
|
||||||
thumbnail = thumbnailUrl ?? undefined;
|
thumbnail = thumbnailUrl ?? undefined;
|
||||||
|
|||||||
@@ -25,7 +25,6 @@ interface CategoryInfo {
|
|||||||
parentId: CategoryId;
|
parentId: CategoryId;
|
||||||
name: string;
|
name: string;
|
||||||
files: { id: number; isRecursive: boolean }[];
|
files: { id: number; isRecursive: boolean }[];
|
||||||
isFileRecursive: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const filesystem = new Dexie("filesystem") as Dexie & {
|
const filesystem = new Dexie("filesystem") as Dexie & {
|
||||||
@@ -34,21 +33,11 @@ const filesystem = new Dexie("filesystem") as Dexie & {
|
|||||||
category: EntityTable<CategoryInfo, "id">;
|
category: EntityTable<CategoryInfo, "id">;
|
||||||
};
|
};
|
||||||
|
|
||||||
filesystem
|
filesystem.version(2).stores({
|
||||||
.version(3)
|
directory: "id, parentId",
|
||||||
.stores({
|
file: "id, parentId",
|
||||||
directory: "id, parentId",
|
category: "id, parentId",
|
||||||
file: "id, parentId",
|
});
|
||||||
category: "id, parentId",
|
|
||||||
})
|
|
||||||
.upgrade(async (trx) => {
|
|
||||||
await trx
|
|
||||||
.table("category")
|
|
||||||
.toCollection()
|
|
||||||
.modify((category) => {
|
|
||||||
category.isFileRecursive = false;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
export const getDirectoryInfos = async (parentId: DirectoryId) => {
|
export const getDirectoryInfos = async (parentId: DirectoryId) => {
|
||||||
return await filesystem.directory.where({ parentId }).toArray();
|
return await filesystem.directory.where({ parentId }).toArray();
|
||||||
@@ -98,10 +87,6 @@ export const storeCategoryInfo = async (categoryInfo: CategoryInfo) => {
|
|||||||
await filesystem.category.put(categoryInfo);
|
await filesystem.category.put(categoryInfo);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const updateCategoryInfo = async (id: number, changes: { isFileRecursive?: boolean }) => {
|
|
||||||
await filesystem.category.update(id, changes);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const deleteCategoryInfo = async (id: number) => {
|
export const deleteCategoryInfo = async (id: number) => {
|
||||||
await filesystem.category.delete(id);
|
await filesystem.category.delete(id);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ import {
|
|||||||
getCategoryInfos as getCategoryInfosFromIndexedDB,
|
getCategoryInfos as getCategoryInfosFromIndexedDB,
|
||||||
getCategoryInfo as getCategoryInfoFromIndexedDB,
|
getCategoryInfo as getCategoryInfoFromIndexedDB,
|
||||||
storeCategoryInfo,
|
storeCategoryInfo,
|
||||||
updateCategoryInfo as updateCategoryInfoInIndexedDB,
|
|
||||||
deleteCategoryInfo,
|
deleteCategoryInfo,
|
||||||
type DirectoryId,
|
type DirectoryId,
|
||||||
type CategoryId,
|
type CategoryId,
|
||||||
@@ -63,7 +62,6 @@ export type CategoryInfo =
|
|||||||
name?: undefined;
|
name?: undefined;
|
||||||
subCategoryIds: number[];
|
subCategoryIds: number[];
|
||||||
files?: undefined;
|
files?: undefined;
|
||||||
isFileRecursive?: undefined;
|
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
id: number;
|
id: number;
|
||||||
@@ -72,7 +70,6 @@ export type CategoryInfo =
|
|||||||
name: string;
|
name: string;
|
||||||
subCategoryIds: number[];
|
subCategoryIds: number[];
|
||||||
files: { id: number; isRecursive: boolean }[];
|
files: { id: number; isRecursive: boolean }[];
|
||||||
isFileRecursive: boolean;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const directoryInfoStore = new Map<DirectoryId, Writable<DirectoryInfo | null>>();
|
const directoryInfoStore = new Map<DirectoryId, Writable<DirectoryInfo | null>>();
|
||||||
@@ -258,13 +255,7 @@ const fetchCategoryInfoFromIndexedDB = async (
|
|||||||
info.set({ id, subCategoryIds });
|
info.set({ id, subCategoryIds });
|
||||||
} else {
|
} else {
|
||||||
if (!category) return;
|
if (!category) return;
|
||||||
info.set({
|
info.set({ id, name: category.name, subCategoryIds, files: category.files });
|
||||||
id,
|
|
||||||
name: category.name,
|
|
||||||
subCategoryIds,
|
|
||||||
files: category.files,
|
|
||||||
isFileRecursive: category.isFileRecursive,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -297,28 +288,20 @@ const fetchCategoryInfoFromServer = async (
|
|||||||
|
|
||||||
const { files }: CategoryFileListResponse = await res.json();
|
const { files }: CategoryFileListResponse = await res.json();
|
||||||
const filesMapped = files.map(({ file, isRecursive }) => ({ id: file, isRecursive }));
|
const filesMapped = files.map(({ file, isRecursive }) => ({ id: file, isRecursive }));
|
||||||
let isFileRecursive: boolean | undefined = undefined;
|
|
||||||
|
|
||||||
info.update((value) => {
|
info.set({
|
||||||
const newValue = {
|
id,
|
||||||
isFileRecursive: false,
|
dataKey,
|
||||||
...value,
|
dataKeyVersion: new Date(metadata!.dekVersion),
|
||||||
id,
|
name,
|
||||||
dataKey,
|
subCategoryIds: subCategories,
|
||||||
dataKeyVersion: new Date(metadata!.dekVersion),
|
files: filesMapped,
|
||||||
name,
|
|
||||||
subCategoryIds: subCategories,
|
|
||||||
files: filesMapped,
|
|
||||||
};
|
|
||||||
isFileRecursive = newValue.isFileRecursive;
|
|
||||||
return newValue;
|
|
||||||
});
|
});
|
||||||
await storeCategoryInfo({
|
await storeCategoryInfo({
|
||||||
id,
|
id,
|
||||||
parentId: metadata!.parent,
|
parentId: metadata!.parent,
|
||||||
name,
|
name,
|
||||||
files: filesMapped,
|
files: filesMapped,
|
||||||
isFileRecursive: isFileRecursive!,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -344,17 +327,3 @@ export const getCategoryInfo = (categoryId: CategoryId, masterKey: CryptoKey) =>
|
|||||||
fetchCategoryInfo(categoryId, info, masterKey); // Intended
|
fetchCategoryInfo(categoryId, info, masterKey); // Intended
|
||||||
return info;
|
return info;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const updateCategoryInfo = async (
|
|
||||||
categoryId: number,
|
|
||||||
changes: { isFileRecursive?: boolean },
|
|
||||||
) => {
|
|
||||||
await updateCategoryInfoInIndexedDB(categoryId, changes);
|
|
||||||
categoryInfoStore.get(categoryId)?.update((value) => {
|
|
||||||
if (!value) return value;
|
|
||||||
if (changes.isFileRecursive !== undefined) {
|
|
||||||
value.isFileRecursive = changes.isFileRecursive;
|
|
||||||
}
|
|
||||||
return value;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ const capture = (
|
|||||||
|
|
||||||
drawer(context, scaledWidth, scaledHeight);
|
drawer(context, scaledWidth, scaledHeight);
|
||||||
canvas.toBlob((blob) => {
|
canvas.toBlob((blob) => {
|
||||||
if (blob && blob.type === "image/webp") {
|
if (blob) {
|
||||||
resolve(blob);
|
resolve(blob);
|
||||||
} else {
|
} else {
|
||||||
reject(new Error("Failed to generate thumbnail"));
|
reject(new Error("Failed to generate thumbnail"));
|
||||||
@@ -67,15 +67,10 @@ const generateVideoThumbnail = (videoUrl: string, time = 0) => {
|
|||||||
return new Promise<Blob>((resolve, reject) => {
|
return new Promise<Blob>((resolve, reject) => {
|
||||||
const video = document.createElement("video");
|
const video = document.createElement("video");
|
||||||
video.onloadedmetadata = () => {
|
video.onloadedmetadata = () => {
|
||||||
if (video.videoWidth === 0 || video.videoHeight === 0) {
|
|
||||||
return reject();
|
|
||||||
}
|
|
||||||
|
|
||||||
const callbackId = video.requestVideoFrameCallback(() => {
|
|
||||||
captureVideoThumbnail(video).then(resolve).catch(reject);
|
|
||||||
video.cancelVideoFrameCallback(callbackId);
|
|
||||||
});
|
|
||||||
video.currentTime = Math.min(time, video.duration);
|
video.currentTime = Math.min(time, video.duration);
|
||||||
|
video.requestVideoFrameCallback(() => {
|
||||||
|
captureVideoThumbnail(video).then(resolve).catch(reject);
|
||||||
|
});
|
||||||
};
|
};
|
||||||
video.onerror = reject;
|
video.onerror = reject;
|
||||||
|
|
||||||
@@ -88,26 +83,18 @@ const generateVideoThumbnail = (videoUrl: string, time = 0) => {
|
|||||||
export const generateThumbnail = async (fileBuffer: ArrayBuffer, fileType: string) => {
|
export const generateThumbnail = async (fileBuffer: ArrayBuffer, fileType: string) => {
|
||||||
let url;
|
let url;
|
||||||
try {
|
try {
|
||||||
if (fileType.startsWith("image/")) {
|
if (fileType === "image/heic") {
|
||||||
const fileBlob = new Blob([fileBuffer], { type: fileType });
|
const { default: heic2any } = await import("heic2any");
|
||||||
url = URL.createObjectURL(fileBlob);
|
url = URL.createObjectURL(
|
||||||
|
(await heic2any({
|
||||||
try {
|
blob: new Blob([fileBuffer], { type: fileType }),
|
||||||
return await generateImageThumbnail(url);
|
toType: "image/png",
|
||||||
} catch {
|
})) as Blob,
|
||||||
URL.revokeObjectURL(url);
|
);
|
||||||
url = undefined;
|
return await generateImageThumbnail(url);
|
||||||
|
} else if (fileType.startsWith("image/")) {
|
||||||
if (fileType === "image/heic") {
|
url = URL.createObjectURL(new Blob([fileBuffer], { type: fileType }));
|
||||||
const { default: heic2any } = await import("heic2any");
|
return await generateImageThumbnail(url);
|
||||||
url = URL.createObjectURL(
|
|
||||||
(await heic2any({ blob: fileBlob, toType: "image/png" })) as Blob,
|
|
||||||
);
|
|
||||||
return await generateImageThumbnail(url);
|
|
||||||
} else {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (fileType.startsWith("video/")) {
|
} else if (fileType.startsWith("video/")) {
|
||||||
url = URL.createObjectURL(new Blob([fileBuffer], { type: fileType }));
|
url = URL.createObjectURL(new Blob([fileBuffer], { type: fileType }));
|
||||||
return await generateVideoThumbnail(url);
|
return await generateVideoThumbnail(url);
|
||||||
|
|||||||
@@ -48,9 +48,9 @@ export const requestFileThumbnailUpload = async (
|
|||||||
return await fetch(`/api/file/${fileId}/thumbnail/upload`, { method: "POST", body: form });
|
return await fetch(`/api/file/${fileId}/thumbnail/upload`, { method: "POST", body: form });
|
||||||
};
|
};
|
||||||
|
|
||||||
export const requestFileThumbnailDownload = async (fileId: number, dataKey?: CryptoKey) => {
|
export const requestFileThumbnailDownload = async (fileId: number, dataKey: CryptoKey) => {
|
||||||
const cache = await getFileThumbnailCache(fileId);
|
const cache = await getFileThumbnailCache(fileId);
|
||||||
if (cache || !dataKey) return cache;
|
if (cache) return cache;
|
||||||
|
|
||||||
let res = await callGetApi(`/api/file/${fileId}/thumbnail`);
|
let res = await callGetApi(`/api/file/${fileId}/thumbnail`);
|
||||||
if (!res.ok) return null;
|
if (!res.ok) return null;
|
||||||
|
|||||||
@@ -6,14 +6,8 @@
|
|||||||
|
|
||||||
let oldPassword = $state("");
|
let oldPassword = $state("");
|
||||||
let newPassword = $state("");
|
let newPassword = $state("");
|
||||||
let confirmPassword = $state("");
|
|
||||||
|
|
||||||
const changePassword = async () => {
|
const changePassword = async () => {
|
||||||
if (newPassword !== confirmPassword) {
|
|
||||||
// TODO: Alert
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (await requestPasswordChange(oldPassword, newPassword)) {
|
if (await requestPasswordChange(oldPassword, newPassword)) {
|
||||||
await goto("/menu");
|
await goto("/menu");
|
||||||
}
|
}
|
||||||
@@ -36,7 +30,6 @@
|
|||||||
|
|
||||||
<TextInput bind:value={oldPassword} placeholder="기존 비밀번호" type="password" />
|
<TextInput bind:value={oldPassword} placeholder="기존 비밀번호" type="password" />
|
||||||
<TextInput bind:value={newPassword} placeholder="새 비밀번호" type="password" />
|
<TextInput bind:value={newPassword} placeholder="새 비밀번호" type="password" />
|
||||||
<TextInput bind:value={confirmPassword} placeholder="새 비밀번호 확인" type="password" />
|
|
||||||
</TitledDiv>
|
</TitledDiv>
|
||||||
<BottomDiv>
|
<BottomDiv>
|
||||||
<Button onclick={changePassword} class="w-full">비밀번호 바꾸기</Button>
|
<Button onclick={changePassword} class="w-full">비밀번호 바꾸기</Button>
|
||||||
|
|||||||
@@ -43,31 +43,22 @@
|
|||||||
let isDownloadRequested = $state(false);
|
let isDownloadRequested = $state(false);
|
||||||
let viewerType: "image" | "video" | undefined = $state();
|
let viewerType: "image" | "video" | undefined = $state();
|
||||||
let fileBlobUrl: string | undefined = $state();
|
let fileBlobUrl: string | undefined = $state();
|
||||||
let heicBlob: Blob | undefined = $state();
|
|
||||||
let videoElement: HTMLVideoElement | undefined = $state();
|
let videoElement: HTMLVideoElement | undefined = $state();
|
||||||
|
|
||||||
const updateViewer = async (buffer: ArrayBuffer, contentType: string) => {
|
const updateViewer = async (buffer: ArrayBuffer, contentType: string) => {
|
||||||
const fileBlob = new Blob([buffer], { type: contentType });
|
const fileBlob = new Blob([buffer], { type: contentType });
|
||||||
if (viewerType) {
|
if (contentType === "image/heic") {
|
||||||
|
const { default: heic2any } = await import("heic2any");
|
||||||
|
fileBlobUrl = URL.createObjectURL(
|
||||||
|
(await heic2any({ blob: fileBlob, toType: "image/jpeg" })) as Blob,
|
||||||
|
);
|
||||||
|
} else if (viewerType) {
|
||||||
fileBlobUrl = URL.createObjectURL(fileBlob);
|
fileBlobUrl = URL.createObjectURL(fileBlob);
|
||||||
heicBlob = contentType === "image/heic" ? fileBlob : undefined;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return fileBlob;
|
return fileBlob;
|
||||||
};
|
};
|
||||||
|
|
||||||
const convertHeicToJpeg = async () => {
|
|
||||||
if (!heicBlob) return;
|
|
||||||
|
|
||||||
URL.revokeObjectURL(fileBlobUrl!);
|
|
||||||
fileBlobUrl = undefined;
|
|
||||||
|
|
||||||
const { default: heic2any } = await import("heic2any");
|
|
||||||
fileBlobUrl = URL.createObjectURL(
|
|
||||||
(await heic2any({ blob: heicBlob, toType: "image/jpeg" })) as Blob,
|
|
||||||
);
|
|
||||||
heicBlob = undefined;
|
|
||||||
};
|
|
||||||
|
|
||||||
const updateThumbnail = async (dataKey: CryptoKey, dataKeyVersion: Date) => {
|
const updateThumbnail = async (dataKey: CryptoKey, dataKeyVersion: Date) => {
|
||||||
const thumbnail = await captureVideoThumbnail(videoElement!);
|
const thumbnail = await captureVideoThumbnail(videoElement!);
|
||||||
await requestThumbnailUpload(data.id, thumbnail, dataKey, dataKeyVersion);
|
await requestThumbnailUpload(data.id, thumbnail, dataKey, dataKeyVersion);
|
||||||
@@ -145,7 +136,7 @@
|
|||||||
|
|
||||||
{#if viewerType === "image"}
|
{#if viewerType === "image"}
|
||||||
{#if fileBlobUrl}
|
{#if fileBlobUrl}
|
||||||
<img src={fileBlobUrl} alt={$info.name} onerror={convertHeicToJpeg} />
|
<img src={fileBlobUrl} alt={$info.name} />
|
||||||
{:else}
|
{:else}
|
||||||
{@render viewerLoading("이미지를 불러오고 있어요.")}
|
{@render viewerLoading("이미지를 불러오고 있어요.")}
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
import { goto } from "$app/navigation";
|
import { goto } from "$app/navigation";
|
||||||
import { TopBar } from "$lib/components/molecules";
|
import { TopBar } from "$lib/components/molecules";
|
||||||
import { Category, CategoryCreateModal } from "$lib/components/organisms";
|
import { Category, CategoryCreateModal } from "$lib/components/organisms";
|
||||||
import { getCategoryInfo, updateCategoryInfo, type CategoryInfo } from "$lib/modules/filesystem";
|
import { getCategoryInfo, type CategoryInfo } from "$lib/modules/filesystem";
|
||||||
import { masterKeyStore } from "$lib/stores";
|
import { masterKeyStore } from "$lib/stores";
|
||||||
import CategoryDeleteModal from "./CategoryDeleteModal.svelte";
|
import CategoryDeleteModal from "./CategoryDeleteModal.svelte";
|
||||||
import CategoryMenuBottomSheet from "./CategoryMenuBottomSheet.svelte";
|
import CategoryMenuBottomSheet from "./CategoryMenuBottomSheet.svelte";
|
||||||
@@ -21,7 +21,7 @@
|
|||||||
|
|
||||||
let info: Writable<CategoryInfo | null> | undefined = $state();
|
let info: Writable<CategoryInfo | null> | undefined = $state();
|
||||||
|
|
||||||
let isFileRecursive: boolean | undefined = $state();
|
let isFileRecursive = $state(false);
|
||||||
|
|
||||||
let isCategoryCreateModalOpen = $state(false);
|
let isCategoryCreateModalOpen = $state(false);
|
||||||
let isCategoryMenuBottomSheetOpen = $state(false);
|
let isCategoryMenuBottomSheetOpen = $state(false);
|
||||||
@@ -30,19 +30,6 @@
|
|||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
info = getCategoryInfo(data.id, $masterKeyStore?.get(1)?.key!);
|
info = getCategoryInfo(data.id, $masterKeyStore?.get(1)?.key!);
|
||||||
isFileRecursive = undefined;
|
|
||||||
});
|
|
||||||
|
|
||||||
$effect(() => {
|
|
||||||
if ($info && isFileRecursive === undefined) {
|
|
||||||
isFileRecursive = $info.isFileRecursive ?? false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
$effect(() => {
|
|
||||||
if (data.id !== "root" && $info?.isFileRecursive !== isFileRecursive) {
|
|
||||||
updateCategoryInfo(data.id as number, { isFileRecursive });
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -54,7 +41,7 @@
|
|||||||
<TopBar title={$info?.name} />
|
<TopBar title={$info?.name} />
|
||||||
{/if}
|
{/if}
|
||||||
<div class="min-h-full bg-gray-100 pb-[5.5em]">
|
<div class="min-h-full bg-gray-100 pb-[5.5em]">
|
||||||
{#if $info && isFileRecursive !== undefined}
|
{#if $info}
|
||||||
<Category
|
<Category
|
||||||
bind:isFileRecursive
|
bind:isFileRecursive
|
||||||
info={$info}
|
info={$info}
|
||||||
|
|||||||
@@ -34,7 +34,7 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
if ($info) {
|
if ($info?.dataKey) {
|
||||||
requestFileThumbnailDownload($info.id, $info.dataKey)
|
requestFileThumbnailDownload($info.id, $info.dataKey)
|
||||||
.then((thumbnailUrl) => {
|
.then((thumbnailUrl) => {
|
||||||
thumbnail = thumbnailUrl ?? undefined;
|
thumbnail = thumbnailUrl ?? undefined;
|
||||||
|
|||||||
Reference in New Issue
Block a user