diff --git a/package.json b/package.json index 67eb017..e91eafa 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "arkvault", "private": true, - "version": "0.5.0", + "version": "0.5.1", "type": "module", "scripts": { "dev": "vite dev", diff --git a/src/lib/indexedDB/filesystem.ts b/src/lib/indexedDB/filesystem.ts index 1c2c060..cf60b93 100644 --- a/src/lib/indexedDB/filesystem.ts +++ b/src/lib/indexedDB/filesystem.ts @@ -25,6 +25,7 @@ interface CategoryInfo { parentId: CategoryId; name: string; files: { id: number; isRecursive: boolean }[]; + isFileRecursive: boolean; } const filesystem = new Dexie("filesystem") as Dexie & { @@ -33,11 +34,21 @@ const filesystem = new Dexie("filesystem") as Dexie & { category: EntityTable; }; -filesystem.version(2).stores({ - directory: "id, parentId", - file: "id, parentId", - category: "id, parentId", -}); +filesystem + .version(3) + .stores({ + directory: "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) => { return await filesystem.directory.where({ parentId }).toArray(); @@ -87,6 +98,10 @@ export const storeCategoryInfo = async (categoryInfo: 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) => { await filesystem.category.delete(id); }; diff --git a/src/lib/modules/filesystem.ts b/src/lib/modules/filesystem.ts index eaf1d1a..c160534 100644 --- a/src/lib/modules/filesystem.ts +++ b/src/lib/modules/filesystem.ts @@ -12,6 +12,7 @@ import { getCategoryInfos as getCategoryInfosFromIndexedDB, getCategoryInfo as getCategoryInfoFromIndexedDB, storeCategoryInfo, + updateCategoryInfo as updateCategoryInfoInIndexedDB, deleteCategoryInfo, type DirectoryId, type CategoryId, @@ -62,6 +63,7 @@ export type CategoryInfo = name?: undefined; subCategoryIds: number[]; files?: undefined; + isFileRecursive?: undefined; } | { id: number; @@ -70,6 +72,7 @@ export type CategoryInfo = name: string; subCategoryIds: number[]; files: { id: number; isRecursive: boolean }[]; + isFileRecursive: boolean; }; const directoryInfoStore = new Map>(); @@ -255,7 +258,13 @@ const fetchCategoryInfoFromIndexedDB = async ( info.set({ id, subCategoryIds }); } else { if (!category) return; - info.set({ id, name: category.name, subCategoryIds, files: category.files }); + info.set({ + id, + name: category.name, + subCategoryIds, + files: category.files, + isFileRecursive: category.isFileRecursive, + }); } }; @@ -288,20 +297,28 @@ const fetchCategoryInfoFromServer = async ( const { files }: CategoryFileListResponse = await res.json(); const filesMapped = files.map(({ file, isRecursive }) => ({ id: file, isRecursive })); + let isFileRecursive: boolean | undefined = undefined; - info.set({ - id, - dataKey, - dataKeyVersion: new Date(metadata!.dekVersion), - name, - subCategoryIds: subCategories, - files: filesMapped, + info.update((value) => { + const newValue = { + isFileRecursive: false, + ...value, + id, + dataKey, + dataKeyVersion: new Date(metadata!.dekVersion), + name, + subCategoryIds: subCategories, + files: filesMapped, + }; + isFileRecursive = newValue.isFileRecursive; + return newValue; }); await storeCategoryInfo({ id, parentId: metadata!.parent, name, files: filesMapped, + isFileRecursive: isFileRecursive!, }); } }; @@ -327,3 +344,17 @@ export const getCategoryInfo = (categoryId: CategoryId, masterKey: CryptoKey) => fetchCategoryInfo(categoryId, info, masterKey); // Intended 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; + }); +}; diff --git a/src/lib/modules/thumbnail.ts b/src/lib/modules/thumbnail.ts index 9c009bd..2d5cd55 100644 --- a/src/lib/modules/thumbnail.ts +++ b/src/lib/modules/thumbnail.ts @@ -32,7 +32,7 @@ const capture = ( drawer(context, scaledWidth, scaledHeight); canvas.toBlob((blob) => { - if (blob) { + if (blob && blob.type === "image/webp") { resolve(blob); } else { reject(new Error("Failed to generate thumbnail")); @@ -83,18 +83,26 @@ const generateVideoThumbnail = (videoUrl: string, time = 0) => { export const generateThumbnail = async (fileBuffer: ArrayBuffer, fileType: string) => { let url; try { - if (fileType === "image/heic") { - const { default: heic2any } = await import("heic2any"); - url = URL.createObjectURL( - (await heic2any({ - blob: new Blob([fileBuffer], { type: fileType }), - toType: "image/png", - })) as Blob, - ); - return await generateImageThumbnail(url); - } else if (fileType.startsWith("image/")) { - url = URL.createObjectURL(new Blob([fileBuffer], { type: fileType })); - return await generateImageThumbnail(url); + if (fileType.startsWith("image/")) { + const fileBlob = new Blob([fileBuffer], { type: fileType }); + url = URL.createObjectURL(fileBlob); + + try { + return await generateImageThumbnail(url); + } catch { + URL.revokeObjectURL(url); + url = undefined; + + if (fileType === "image/heic") { + const { default: heic2any } = await import("heic2any"); + url = URL.createObjectURL( + (await heic2any({ blob: fileBlob, toType: "image/png" })) as Blob, + ); + return await generateImageThumbnail(url); + } else { + return null; + } + } } else if (fileType.startsWith("video/")) { url = URL.createObjectURL(new Blob([fileBuffer], { type: fileType })); return await generateVideoThumbnail(url); diff --git a/src/routes/(fullscreen)/auth/changePassword/+page.svelte b/src/routes/(fullscreen)/auth/changePassword/+page.svelte index 25ded91..b8cabff 100644 --- a/src/routes/(fullscreen)/auth/changePassword/+page.svelte +++ b/src/routes/(fullscreen)/auth/changePassword/+page.svelte @@ -6,8 +6,14 @@ let oldPassword = $state(""); let newPassword = $state(""); + let confirmPassword = $state(""); const changePassword = async () => { + if (newPassword !== confirmPassword) { + // TODO: Alert + return; + } + if (await requestPasswordChange(oldPassword, newPassword)) { await goto("/menu"); } @@ -30,6 +36,7 @@ + diff --git a/src/routes/(fullscreen)/file/[id]/+page.svelte b/src/routes/(fullscreen)/file/[id]/+page.svelte index 61465bd..4047e7c 100644 --- a/src/routes/(fullscreen)/file/[id]/+page.svelte +++ b/src/routes/(fullscreen)/file/[id]/+page.svelte @@ -43,22 +43,31 @@ let isDownloadRequested = $state(false); let viewerType: "image" | "video" | undefined = $state(); let fileBlobUrl: string | undefined = $state(); + let heicBlob: Blob | undefined = $state(); let videoElement: HTMLVideoElement | undefined = $state(); const updateViewer = async (buffer: ArrayBuffer, contentType: string) => { const fileBlob = new Blob([buffer], { type: contentType }); - 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) { + if (viewerType) { fileBlobUrl = URL.createObjectURL(fileBlob); + heicBlob = contentType === "image/heic" ? fileBlob : undefined; } - 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 thumbnail = await captureVideoThumbnail(videoElement!); await requestThumbnailUpload(data.id, thumbnail, dataKey, dataKeyVersion); @@ -136,7 +145,7 @@ {#if viewerType === "image"} {#if fileBlobUrl} - {$info.name} + {$info.name} {:else} {@render viewerLoading("이미지를 불러오고 있어요.")} {/if} diff --git a/src/routes/(main)/category/[[id]]/+page.svelte b/src/routes/(main)/category/[[id]]/+page.svelte index 9bfa2a4..9cbc41c 100644 --- a/src/routes/(main)/category/[[id]]/+page.svelte +++ b/src/routes/(main)/category/[[id]]/+page.svelte @@ -3,7 +3,7 @@ import { goto } from "$app/navigation"; import { TopBar } from "$lib/components/molecules"; import { Category, CategoryCreateModal } from "$lib/components/organisms"; - import { getCategoryInfo, type CategoryInfo } from "$lib/modules/filesystem"; + import { getCategoryInfo, updateCategoryInfo, type CategoryInfo } from "$lib/modules/filesystem"; import { masterKeyStore } from "$lib/stores"; import CategoryDeleteModal from "./CategoryDeleteModal.svelte"; import CategoryMenuBottomSheet from "./CategoryMenuBottomSheet.svelte"; @@ -21,7 +21,7 @@ let info: Writable | undefined = $state(); - let isFileRecursive = $state(false); + let isFileRecursive: boolean | undefined = $state(); let isCategoryCreateModalOpen = $state(false); let isCategoryMenuBottomSheetOpen = $state(false); @@ -30,6 +30,19 @@ $effect(() => { 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 }); + } }); @@ -41,7 +54,7 @@ {/if}
- {#if $info} + {#if $info && isFileRecursive !== undefined}