From e4cce6b8a0d40773cea773cfc7f25b6563fbb842 Mon Sep 17 00:00:00 2001 From: static Date: Mon, 7 Jul 2025 00:30:38 +0900 Subject: [PATCH] =?UTF-8?q?OPFS=EC=97=90=20=EC=BA=90=EC=8B=9C=EB=90=9C=20?= =?UTF-8?q?=EC=8D=B8=EB=84=A4=EC=9D=BC=EC=9D=84=20=EB=AA=A8=EB=91=90=20?= =?UTF-8?q?=EC=82=AD=EC=A0=9C=ED=95=98=EB=8A=94=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../atoms/divs/FullscreenDiv.svelte | 12 +++- src/lib/modules/file/thumbnail.ts | 7 ++- src/lib/modules/opfs.ts | 36 +++++++++++ .../(fullscreen)/settings/cache/+page.svelte | 3 +- .../(fullscreen)/settings/cache/service.ts | 5 -- .../settings/thumbnails/+page.svelte | 63 +++++++++++-------- .../settings/thumbnails/File.svelte | 2 +- .../settings/thumbnails/service.svelte.ts | 2 +- 8 files changed, 91 insertions(+), 39 deletions(-) delete mode 100644 src/routes/(fullscreen)/settings/cache/service.ts diff --git a/src/lib/components/atoms/divs/FullscreenDiv.svelte b/src/lib/components/atoms/divs/FullscreenDiv.svelte index c90e02c..4bb1cc0 100644 --- a/src/lib/components/atoms/divs/FullscreenDiv.svelte +++ b/src/lib/components/atoms/divs/FullscreenDiv.svelte @@ -1,7 +1,15 @@ -
+
{@render children()}
diff --git a/src/lib/modules/file/thumbnail.ts b/src/lib/modules/file/thumbnail.ts index e78786c..6757ffc 100644 --- a/src/lib/modules/file/thumbnail.ts +++ b/src/lib/modules/file/thumbnail.ts @@ -1,5 +1,5 @@ import { LRUCache } from "lru-cache"; -import { readFile, writeFile, deleteFile } from "$lib/modules/opfs"; +import { readFile, writeFile, deleteFile, deleteDirectory } from "$lib/modules/opfs"; import { getThumbnailUrl } from "$lib/modules/thumbnail"; const loadedThumbnails = new LRUCache({ max: 100 }); @@ -27,3 +27,8 @@ export const deleteFileThumbnail = async (fileId: number) => { loadedThumbnails.delete(fileId); await deleteFile(`/thumbnails/${fileId}`); }; + +export const deleteAllFileThumbnails = async () => { + loadedThumbnails.clear(); + await deleteDirectory("/thumbnails"); +}; diff --git a/src/lib/modules/opfs.ts b/src/lib/modules/opfs.ts index 5ac70da..41f1f72 100644 --- a/src/lib/modules/opfs.ts +++ b/src/lib/modules/opfs.ts @@ -59,3 +59,39 @@ export const deleteFile = async (path: string) => { await parentHandle.removeEntry(filename); }; + +const getDirectoryHandle = async (path: string) => { + if (!rootHandle) { + throw new Error("OPFS not prepared"); + } else if (path[0] !== "/") { + throw new Error("Path must be absolute"); + } + + const parts = path.split("/"); + if (parts.length <= 1) { + throw new Error("Invalid path"); + } + + try { + let directoryHandle = rootHandle; + let parentHandle; + for (const part of parts.slice(1)) { + if (!part) continue; + parentHandle = directoryHandle; + directoryHandle = await directoryHandle.getDirectoryHandle(part); + } + return { directoryHandle, parentHandle }; + } catch (e) { + if (e instanceof DOMException && e.name === "NotFoundError") { + return {}; + } + throw e; + } +}; + +export const deleteDirectory = async (path: string) => { + const { directoryHandle, parentHandle } = await getDirectoryHandle(path); + if (!parentHandle) return; + + await parentHandle.removeEntry(directoryHandle.name, { recursive: true }); +}; diff --git a/src/routes/(fullscreen)/settings/cache/+page.svelte b/src/routes/(fullscreen)/settings/cache/+page.svelte index 262aacf..af375c2 100644 --- a/src/routes/(fullscreen)/settings/cache/+page.svelte +++ b/src/routes/(fullscreen)/settings/cache/+page.svelte @@ -4,12 +4,11 @@ import { FullscreenDiv } from "$lib/components/atoms"; import { TopBar } from "$lib/components/molecules"; import type { FileCacheIndex } from "$lib/indexedDB"; - import { getFileCacheIndex } from "$lib/modules/file"; + import { getFileCacheIndex, deleteFileCache as doDeleteFileCache } from "$lib/modules/file"; import { getFileInfo, type FileInfo } from "$lib/modules/filesystem"; import { formatFileSize } from "$lib/modules/util"; import { masterKeyStore } from "$lib/stores"; import File from "./File.svelte"; - import { deleteFileCache as doDeleteFileCache } from "./service"; interface FileCache { index: FileCacheIndex; diff --git a/src/routes/(fullscreen)/settings/cache/service.ts b/src/routes/(fullscreen)/settings/cache/service.ts deleted file mode 100644 index 35b0251..0000000 --- a/src/routes/(fullscreen)/settings/cache/service.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { deleteFileCache as doDeleteFileCache } from "$lib/modules/file"; - -export const deleteFileCache = async (fileId: number) => { - await doDeleteFileCache(fileId); -}; diff --git a/src/routes/(fullscreen)/settings/thumbnails/+page.svelte b/src/routes/(fullscreen)/settings/thumbnails/+page.svelte index 5be902e..a498f1c 100644 --- a/src/routes/(fullscreen)/settings/thumbnails/+page.svelte +++ b/src/routes/(fullscreen)/settings/thumbnails/+page.svelte @@ -3,23 +3,26 @@ import { get } from "svelte/store"; import { goto } from "$app/navigation"; import { BottomDiv, Button, FullscreenDiv } from "$lib/components/atoms"; - import { TopBar } from "$lib/components/molecules"; + import { IconEntryButton, TopBar } from "$lib/components/molecules"; + import { deleteAllFileThumbnails } from "$lib/modules/file"; import { getFileInfo } from "$lib/modules/filesystem"; import { masterKeyStore } from "$lib/stores"; import File from "./File.svelte"; import { persistentStates, getGenerationStatus, - requestFileThumbnailGeneration, + requestThumbnailGeneration, } from "./service.svelte"; + import IconDelete from "~icons/material-symbols/delete"; + let { data } = $props(); - const generateAllThumbnails = async () => { + const generateAllThumbnails = () => { persistentStates.files.forEach(({ info }) => { const fileInfo = get(info); if (fileInfo) { - requestFileThumbnailGeneration(fileInfo); + requestThumbnailGeneration(fileInfo); } }); }; @@ -38,31 +41,37 @@ - - {#if persistentStates.files.length > 0} -
-
-

- {persistentStates.files.length}개 파일의 썸네일이 존재하지 않아요. -

-
-
- {#each persistentStates.files as { info, status }} - goto(`/file/${id}`)} - onGenerateThumbnailClick={requestFileThumbnailGeneration} - /> - {/each} -
+ +
+
+ + 저장된 썸네일 모두 삭제하기 +
- + {#if persistentStates.files.length > 0} +
+

썸네일이 누락된 파일

+
+

+ {persistentStates.files.length}개 파일의 썸네일이 존재하지 않아요. +

+
+ {#each persistentStates.files as { info, status }} + goto(`/file/${id}`)} + onGenerateThumbnailClick={requestThumbnailGeneration} + /> + {/each} +
+
+
+ {/if} +
+ {#if persistentStates.files.length > 0} + - {:else} -
-

모든 파일의 썸네일이 존재해요.

-
{/if}
diff --git a/src/routes/(fullscreen)/settings/thumbnails/File.svelte b/src/routes/(fullscreen)/settings/thumbnails/File.svelte index a9530e1..8d413ec 100644 --- a/src/routes/(fullscreen)/settings/thumbnails/File.svelte +++ b/src/routes/(fullscreen)/settings/thumbnails/File.svelte @@ -32,7 +32,7 @@ onclick($info)} - actionButtonIcon={IconCamera} + actionButtonIcon={!$generationStatus || $generationStatus === "error" ? IconCamera : undefined} onActionButtonClick={() => onGenerateThumbnailClick($info)} actionButtonClass="text-gray-800" > diff --git a/src/routes/(fullscreen)/settings/thumbnails/service.svelte.ts b/src/routes/(fullscreen)/settings/thumbnails/service.svelte.ts index 66d3e18..a3f77ce 100644 --- a/src/routes/(fullscreen)/settings/thumbnails/service.svelte.ts +++ b/src/routes/(fullscreen)/settings/thumbnails/service.svelte.ts @@ -110,7 +110,7 @@ const requestThumbnailUpload = limitFunction( { concurrency: 4 }, ); -export const requestFileThumbnailGeneration = async (fileInfo: FileInfo) => { +export const requestThumbnailGeneration = async (fileInfo: FileInfo) => { let status = workingFiles.get(fileInfo.id); if (status && get(status) !== "error") return;