diff --git a/src/lib/client/cache/thumbnailCache.svelte.ts b/src/lib/client/cache/thumbnailCache.svelte.ts index fc68558..5a36e3d 100644 --- a/src/lib/client/cache/thumbnailCache.svelte.ts +++ b/src/lib/client/cache/thumbnailCache.svelte.ts @@ -1,6 +1,7 @@ +/* eslint-disable svelte/prefer-svelte-reactivity -- Lookup tables of reactive entries */ import { LRUCache } from "lru-cache"; import { deleteDirectory, deleteFile, readFile, writeFile } from "#lib/client/storage/opfs.js"; -import type { SummarizedFileInfo } from "#lib/modules/filesystem/index.js"; +import type { SummarizedFileInfo } from "#lib/features/filesystem/index.js"; import { decryptChunk, encodeToBase64 } from "#lib/shared/crypto/index.js"; export interface Thumbnail { diff --git a/src/lib/client/keys/sync.ts b/src/lib/client/keys/sync.ts index dfb68d4..c16c14c 100644 --- a/src/lib/client/keys/sync.ts +++ b/src/lib/client/keys/sync.ts @@ -63,8 +63,8 @@ export const requestClientRegistration = async () => { answerSig: await answerChallenge(challenge, clientKeys), }); return true; - } catch { - // TODO: Error Handling + } catch (e) { + console.error(e); return false; } }; @@ -95,9 +95,9 @@ export const registerInitialMasterKeyAndHmacSecret = async ( }); } catch (e) { if (isTRPCClientError(e) && (e.data?.code === "FORBIDDEN" || e.data?.code === "CONFLICT")) { - return true; + return true; // Already registered by another client } - // TODO: Error Handling + console.error(e); return false; } @@ -107,8 +107,8 @@ export const registerInitialMasterKeyAndHmacSecret = async ( hsk: hmacSecretWrapped, }); return true; - } catch { - // TODO: Error Handling + } catch (e) { + console.error(e); return false; } }; @@ -117,8 +117,8 @@ export const downloadHmacSecrets = async () => { let hmacSecretsWrapped; try { hmacSecretsWrapped = await trpc.hsk.list.query(); - } catch { - // TODO: Error Handling + } catch (e) { + console.error(e); return false; } @@ -143,8 +143,8 @@ export const downloadMasterKeys = async () => { let masterKeysWrapped; try { masterKeysWrapped = await trpc.mek.list.query(); - } catch { - // TODO: Error Handling + } catch (e) { + console.error(e); return false; } diff --git a/src/lib/client/toast.svelte.ts b/src/lib/client/toast.svelte.ts new file mode 100644 index 0000000..0b147ce --- /dev/null +++ b/src/lib/client/toast.svelte.ts @@ -0,0 +1,49 @@ +import { isTRPCClientError } from "#trpc/client.js"; + +export interface Toast { + id: number; + message: string; +} + +const TOAST_DURATION = 3000; + +let toasts: Toast[] = $state([]); +let nextId = 0; + +export const getToasts = () => toasts; + +export const showToast = (message: string) => { + const toast = { id: nextId++, message }; + toasts.push(toast); + setTimeout(() => { + toasts = toasts.filter(({ id }) => id !== toast.id); + }, TOAST_DURATION); +}; + +const getErrorMessage = (e: unknown, fallbackMessage: string) => { + if (isTRPCClientError(e)) { + switch (e.data?.code) { + case "NOT_FOUND": + return "대상을 찾을 수 없어요. 이미 삭제되었을 수 있어요."; + case undefined: + return "서버에 연결할 수 없어요."; + } + } + return fallbackMessage; +}; + +export const showErrorToast = (e: unknown, fallbackMessage: string) => { + console.error(e); + showToast(getErrorMessage(e, fallbackMessage)); +}; + +// Runs the action and shows the error to the user if it fails. Returns whether it succeeded. +export const tryAction = async (action: () => Promise, errorMessage: string) => { + try { + await action(); + return true; + } catch (e) { + showErrorToast(e, errorMessage); + return false; + } +}; diff --git a/src/lib/components/atoms/Toasts.svelte b/src/lib/components/atoms/Toasts.svelte new file mode 100644 index 0000000..66c28a4 --- /dev/null +++ b/src/lib/components/atoms/Toasts.svelte @@ -0,0 +1,16 @@ + + +
+ {#each getToasts() as toast (toast.id)} + + {/each} +
diff --git a/src/lib/components/atoms/buttons/FileThumbnailButton.svelte b/src/lib/components/atoms/buttons/FileThumbnailButton.svelte index b8dfdb1..098be9b 100644 --- a/src/lib/components/atoms/buttons/FileThumbnailButton.svelte +++ b/src/lib/components/atoms/buttons/FileThumbnailButton.svelte @@ -1,6 +1,6 @@ @@ -93,6 +97,6 @@ requestLogout().catch(console.error)} onLoginClick={() => upgradeSession(true)} /> diff --git a/src/routes/(fullscreen)/auth/login/service.ts b/src/routes/(fullscreen)/auth/login/service.ts deleted file mode 100644 index 620e90b..0000000 --- a/src/routes/(fullscreen)/auth/login/service.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { trpc } from "#trpc/client.js"; - -export const requestLogin = async (email: string, password: string) => { - try { - await trpc.auth.login.mutate({ email, password }); - return true; - } catch { - // TODO: Error Handling - return false; - } -}; diff --git a/src/routes/(fullscreen)/file/[id]/+page.svelte b/src/routes/(fullscreen)/file/[id]/+page.svelte index 59b5bcf..3562b0b 100644 --- a/src/routes/(fullscreen)/file/[id]/+page.svelte +++ b/src/routes/(fullscreen)/file/[id]/+page.svelte @@ -5,20 +5,22 @@ import { page } from "$app/state"; import { FullscreenDiv } from "#lib/components/atoms/index.js"; import { Categories, IconEntryButton, TopBar } from "#lib/components/molecules/index.js"; - import { getFileInfo, type MaybeFileInfo } from "#lib/modules/filesystem/index.js"; import { captureVideoThumbnail } from "#lib/client/media/thumbnail.js"; import { getFileDownloadState } from "#lib/client/transfer/index.js"; - import { HybridPromise } from "#lib/utils/index.js"; + import { showErrorToast, tryAction } from "#lib/client/toast.svelte.js"; + import { + addFileToCategory, + getFileContent, + getFileStreamUrl, + removeFileFromCategory, + setFileFavorite, + uploadFileThumbnail, + useFile, + type DataKey, + type FileInfo, + } from "#lib/features/filesystem/index.js"; import AddToCategoryBottomSheet from "./AddToCategoryBottomSheet.svelte"; import DownloadStatus from "./DownloadStatus.svelte"; - import { - requestFileRemovalFromCategory, - requestFileDownload, - requestThumbnailUpload, - requestFileAdditionToCategory, - requestVideoStream, - requestFavoriteToggle, - } from "./service"; import TopBarMenu from "./TopBarMenu.svelte"; import IconMoreVert from "~icons/material-symbols/more-vert"; @@ -28,7 +30,8 @@ let { data } = $props(); - let info: MaybeFileInfo | undefined = $state(); + const file = useFile(() => data.id); + let info = $derived(file.current); let downloadState = $derived(getFileDownloadState(data.id)); let isMenuOpen = $state(false); @@ -59,33 +62,43 @@ ); }; - const updateThumbnail = async (dataKey: CryptoKey, dataKeyVersion: Date) => { - const thumbnail = await captureVideoThumbnail(videoElement!); - await requestThumbnailUpload(data.id, thumbnail, dataKey, dataKeyVersion); - }; + const updateThumbnail = (dataKey: DataKey) => + tryAction(async () => { + const thumbnail = await captureVideoThumbnail(videoElement!); + await uploadFileThumbnail(data.id, thumbnail, dataKey); + }, "썸네일을 설정하지 못했어요."); const addToCategory = async (categoryId: number) => { - await requestFileAdditionToCategory(data.id, categoryId); - isAddToCategoryBottomSheetOpen = false; - void getFileInfo(data.id); // TODO: FIXME + if ( + await tryAction( + () => addFileToCategory(data.id, categoryId), + "카테고리에 파일을 추가하지 못했어요.", + ) + ) { + isAddToCategoryBottomSheetOpen = false; + } }; - const removeFromCategory = async (categoryId: number) => { - await requestFileRemovalFromCategory(data.id, categoryId); - void getFileInfo(data.id); // TODO: FIXME - }; + const removeFromCategory = (categoryId: number) => + tryAction( + () => removeFileFromCategory(data.id, categoryId), + "카테고리에서 파일을 제거하지 못했어요.", + ); - const toggleFavorite = async () => { - await requestFavoriteToggle(data.id, !!info?.isFavorite); - void getFileInfo(data.id); // TODO: FIXME - }; + const toggleFavorite = () => + tryAction( + () => setFileFavorite(info as FileInfo, !info?.isFavorite), + "즐겨찾기를 변경하지 못했어요.", + ); - $effect(() => { - HybridPromise.resolve(getFileInfo(data.id)).then((result) => { - if (data.id === result.id) { - info = result; - } + const loadContent = (dataKey: CryptoKey, isLegacy: boolean) => + getFileContent(data.id, dataKey, isLegacy).catch((e) => { + showErrorToast(e, "파일을 불러오지 못했어요."); + throw e; }); + + $effect.pre(() => { + void data.id; // Reset the viewer when navigating to another file isDownloadRequested = false; viewerType = undefined; }); @@ -103,25 +116,24 @@ if (!downloadState && !isDownloadRequested) { isDownloadRequested = true; - if (viewerType === "video" && !info!.isLegacy) { - requestVideoStream(data.id, info!.dataKey!.key, contentType).then((streamUrl) => { - if (streamUrl) { + const { key } = info!.dataKey!; + const isLegacy = !!info!.isLegacy; + if (viewerType === "video" && !isLegacy) { + getFileStreamUrl(data.id, key, contentType) + .then((streamUrl) => { videoStreamUrl = streamUrl; - } else { - requestFileDownload(data.id, info!.dataKey!.key, info!.isLegacy!).then((buffer) => - updateViewer(buffer, contentType), - ); + }) + .catch(async () => { + // Fall back to downloading the whole file + await updateViewer(await loadContent(key, isLegacy), contentType); + }); + } else { + void loadContent(key, isLegacy).then(async (buffer) => { + const blob = await updateViewer(buffer, contentType); + if (!viewerType) { + FileSaver.saveAs(blob, info!.name); } }); - } else { - requestFileDownload(data.id, info!.dataKey!.key, info!.isLegacy!).then( - async (buffer) => { - const blob = await updateViewer(buffer, contentType); - if (!viewerType) { - FileSaver.saveAs(blob, info!.name); - } - }, - ); } } }); @@ -192,7 +204,7 @@ > updateThumbnail(info?.dataKey?.key!, info?.dataKey?.version!)} + onclick={() => updateThumbnail(info!.dataKey!)} class="w-full" > 이 장면을 썸네일로 설정하기 diff --git a/src/routes/(fullscreen)/file/[id]/AddToCategoryBottomSheet.svelte b/src/routes/(fullscreen)/file/[id]/AddToCategoryBottomSheet.svelte index 69ac8cc..b2bb9c0 100644 --- a/src/routes/(fullscreen)/file/[id]/AddToCategoryBottomSheet.svelte +++ b/src/routes/(fullscreen)/file/[id]/AddToCategoryBottomSheet.svelte @@ -2,9 +2,8 @@ import { BottomDiv, BottomSheet, Button, FullscreenDiv } from "#lib/components/atoms/index.js"; import { SubCategories } from "#lib/components/molecules/index.js"; import { CategoryCreateModal } from "#lib/components/organisms/index.js"; - import { getCategoryInfo, type MaybeCategoryInfo } from "#lib/modules/filesystem/index.js"; - import { HybridPromise } from "#lib/utils/index.js"; - import { requestCategoryCreation } from "./service"; + import { tryAction } from "#lib/client/toast.svelte.js"; + import { createCategory, useCategory } from "#lib/features/filesystem/index.js"; interface Props { onAddToCategoryClick: (categoryId: number) => void; @@ -13,13 +12,15 @@ let { onAddToCategoryClick, isOpen = $bindable() }: Props = $props(); - let categoryInfo: MaybeCategoryInfo | undefined = $state(); + let categoryId: CategoryId = $state("root"); + const category = useCategory(() => categoryId); + let categoryInfo = $derived(category.current); let isCategoryCreateModalOpen = $state(false); - $effect(() => { + $effect.pre(() => { if (isOpen) { - HybridPromise.resolve(getCategoryInfo("root")).then((result) => (categoryInfo = result)); + categoryId = "root"; } }); @@ -30,8 +31,7 @@ - HybridPromise.resolve(getCategoryInfo(id)).then((result) => (categoryInfo = result))} + onSubCategoryClick={({ id }) => (categoryId = id)} onSubCategoryCreateClick={() => (isCategoryCreateModalOpen = true)} subCategoryCreatePosition="top" /> @@ -48,11 +48,6 @@ { - if (await requestCategoryCreation(name, categoryInfo!.id)) { - void getCategoryInfo(categoryInfo!.id); // TODO: FIXME - return true; - } - return false; - }} + onCreateClick={(name: string) => + tryAction(() => createCategory(categoryId, name), "카테고리를 만들지 못했어요.")} /> diff --git a/src/routes/(fullscreen)/file/[id]/service.ts b/src/routes/(fullscreen)/file/[id]/service.ts deleted file mode 100644 index d562940..0000000 --- a/src/routes/(fullscreen)/file/[id]/service.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { storeFileThumbnailCache } from "#lib/client/cache/index.js"; -import { prepareFileDecryption, getDecryptedFileUrl } from "#lib/client/serviceWorker.js"; -import { requestFileThumbnailUpload } from "#lib/services/file.js"; -import { trpc } from "#trpc/client.js"; - -export { requestCategoryCreation, requestFileRemovalFromCategory } from "#lib/services/category.js"; -export { requestFileDownload } from "#lib/services/file.js"; - -export const requestVideoStream = async ( - fileId: number, - dataKey: CryptoKey, - contentType: string, -) => { - const res = await fetch(`/api/file/${fileId}/download`, { method: "HEAD" }); - if (!res.ok) return null; - - const encContentSize = parseInt(res.headers.get("Content-Length") ?? "0", 10); - if (encContentSize <= 0) return null; - - try { - await prepareFileDecryption(fileId, { isLegacy: false, dataKey, encContentSize, contentType }); - return getDecryptedFileUrl(fileId); - } catch { - // TODO: Error Handling - return null; - } -}; - -export const requestThumbnailUpload = async ( - fileId: number, - thumbnail: Blob, - dataKey: CryptoKey, - dataKeyVersion: Date, -) => { - const res = await requestFileThumbnailUpload(fileId, thumbnail, dataKey, dataKeyVersion); - if (!res) return false; - - void thumbnail.arrayBuffer().then((buffer) => storeFileThumbnailCache(fileId, buffer)); - return true; -}; - -export const requestFileAdditionToCategory = async (fileId: number, categoryId: number) => { - try { - await trpc.category.addFile.mutate({ id: categoryId, file: fileId }); - return true; - } catch { - // TODO: Error Handling - return false; - } -}; - -export const requestFavoriteToggle = async (fileId: number, isFavorite: boolean) => { - try { - if (isFavorite) { - await trpc.favorites.removeFile.mutate({ id: fileId }); - } else { - await trpc.favorites.addFile.mutate({ id: fileId }); - } - return true; - } catch { - // TODO: Error Handling - return false; - } -}; diff --git a/src/routes/(fullscreen)/file/downloads/+page.svelte b/src/routes/(fullscreen)/file/downloads/+page.svelte index 0f6c766..4a3833b 100644 --- a/src/routes/(fullscreen)/file/downloads/+page.svelte +++ b/src/routes/(fullscreen)/file/downloads/+page.svelte @@ -7,7 +7,7 @@ clearDownloadedFiles, type FileDownloadState, } from "#lib/client/transfer/index.js"; - import { bulkGetFileInfo, type MaybeFileInfo } from "#lib/modules/filesystem/index.js"; + import { bulkGetFileInfo, type MaybeFileInfo } from "#lib/features/filesystem/index.js"; import File from "./File.svelte"; let downloadingFiles: { info: MaybeFileInfo; state: FileDownloadState }[] = $state([]); diff --git a/src/routes/(fullscreen)/file/downloads/File.svelte b/src/routes/(fullscreen)/file/downloads/File.svelte index d8bc3d4..f5494af 100644 --- a/src/routes/(fullscreen)/file/downloads/File.svelte +++ b/src/routes/(fullscreen)/file/downloads/File.svelte @@ -1,6 +1,6 @@ diff --git a/src/routes/(fullscreen)/key/generate/+page.svelte b/src/routes/(fullscreen)/key/generate/+page.svelte index 3e79117..e7624bf 100644 --- a/src/routes/(fullscreen)/key/generate/+page.svelte +++ b/src/routes/(fullscreen)/key/generate/+page.svelte @@ -13,7 +13,8 @@ registerInitialMasterKeyAndHmacSecret, requestClientRegistrationAndSessionUpgrade, } from "#lib/client/keys/index.js"; - import { requestDeletedFilesCleanup } from "#lib/services/file.js"; + import { showToast } from "#lib/client/toast.svelte.js"; + import { cleanupDeletedFileCaches } from "#lib/features/filesystem/index.js"; import { gotoStateful } from "#lib/utils/index.js"; import Order from "./Order.svelte"; @@ -46,8 +47,6 @@ ]; const generateKeys = async () => { - // TODO: Loading indicator - const { encryptKey, ...clientKeys } = await generateClientKeys(); const { masterKey, masterKeyWrapped } = await generateInitialMasterKey(encryptKey); const { hmacSecretWrapped } = await generateInitialHmacSecret(masterKey); @@ -66,7 +65,7 @@ isForceLoginModalOpen = true; return; } else if (!upgradeRes) { - // TODO: Error Handling + showToast("이 디바이스를 등록하지 못했어요."); return; } @@ -78,11 +77,11 @@ await keyring.persistClientKeys(); if (!(await registerInitialMasterKeyAndHmacSecret(masterKeyWrapped, hmacSecretWrapped))) { - // TODO: Error Handling + showToast("암호 키를 등록하지 못했어요."); return; } - await requestDeletedFilesCleanup(); + await cleanupDeletedFileCaches().catch(console.error); await goto("/client/pending?redirect=" + encodeURIComponent(data.redirectPath)); }; @@ -90,10 +89,10 @@ const file = fileInput?.files?.[0]; if (!file) return; - if (await importClientKeys(await file.text())) { + if (await importClientKeys(await file.text()).catch(() => false)) { await upgradeSession(false); } else { - // TODO: Error Handling + showToast("올바른 암호 키 파일이 아니에요."); } fileInput!.value = ""; diff --git a/src/routes/(fullscreen)/search/+page.svelte b/src/routes/(fullscreen)/search/+page.svelte index 5d7ab53..e45440a 100644 --- a/src/routes/(fullscreen)/search/+page.svelte +++ b/src/routes/(fullscreen)/search/+page.svelte @@ -5,17 +5,18 @@ import { slide } from "svelte/transition"; import { goto } from "$app/navigation"; import { Chip, FullscreenDiv, RowVirtualizer } from "#lib/components/atoms/index.js"; + import { showErrorToast } from "#lib/client/toast.svelte.js"; import { getDirectoryInfo, type LocalCategoryInfo, type MaybeDirectoryInfo, - } from "#lib/modules/filesystem/index.js"; - import { HybridPromise, searchString, sortEntries } from "#lib/utils/index.js"; + } from "#lib/features/filesystem/index.js"; + import { search, type SearchFilter, type SearchResult } from "#lib/features/search/index.js"; + import { searchString, sortEntries } from "#lib/utils/index.js"; import Directory from "./Directory.svelte"; import File from "./File.svelte"; import SearchBar from "./SearchBar.svelte"; import SelectCategoryBottomSheet from "./SelectCategoryBottomSheet.svelte"; - import { requestSearch, type SearchFilter, type SearchResult } from "./service"; let { data } = $props(); @@ -121,10 +122,18 @@ $effect.pre(() => { if (data.directoryId) { - HybridPromise.resolve(getDirectoryInfo(data.directoryId)).then((res) => { + const applyDirectoryInfo = (res: MaybeDirectoryInfo) => { directoryInfo = res; filters.searchInDirectory = res.exists; - }); + }; + + // Apply synchronously if cached so that a restored snapshot isn't overwritten later + const res = getDirectoryInfo(data.directoryId); + if (res instanceof Promise) { + res.then(applyDirectoryInfo).catch(() => {}); + } else { + applyDirectoryInfo(res); + } } else { directoryInfo = undefined; filters.searchInDirectory = false; @@ -148,13 +157,15 @@ } if (hasAnyFilter) { - requestSearch({ + search({ ancestorId: filters.searchInDirectory ? data.directoryId! : "root", inFavorites: filters.searchInFavorites, categories: filters.categories, - }).then((res) => { - serverResult = res; - }); + }) + .then((res) => { + serverResult = res; + }) + .catch((e) => showErrorToast(e, "검색하지 못했어요.")); } }); diff --git a/src/routes/(fullscreen)/search/Directory.svelte b/src/routes/(fullscreen)/search/Directory.svelte index 48e3833..09fd876 100644 --- a/src/routes/(fullscreen)/search/Directory.svelte +++ b/src/routes/(fullscreen)/search/Directory.svelte @@ -1,7 +1,7 @@ @@ -31,8 +28,7 @@ - HybridPromise.resolve(getCategoryInfo(id)).then((result) => (categoryInfo = result))} + onSubCategoryClick={({ id }) => (categoryId = id)} /> {#if categoryInfo.id !== "root"} diff --git a/src/routes/(fullscreen)/settings/cache/+page.svelte b/src/routes/(fullscreen)/settings/cache/+page.svelte index 062073f..c24d936 100644 --- a/src/routes/(fullscreen)/settings/cache/+page.svelte +++ b/src/routes/(fullscreen)/settings/cache/+page.svelte @@ -7,7 +7,7 @@ getFileCacheIndex, deleteFileCache as doDeleteFileCache, } from "#lib/client/cache/index.js"; - import { bulkGetFileInfo, type MaybeFileInfo } from "#lib/modules/filesystem/index.js"; + import { bulkGetFileInfo, type MaybeFileInfo } from "#lib/features/filesystem/index.js"; import { formatFileSize } from "#lib/utils/index.js"; import File from "./File.svelte"; diff --git a/src/routes/(fullscreen)/settings/cache/File.svelte b/src/routes/(fullscreen)/settings/cache/File.svelte index 1724f5c..501c6db 100644 --- a/src/routes/(fullscreen)/settings/cache/File.svelte +++ b/src/routes/(fullscreen)/settings/cache/File.svelte @@ -1,6 +1,6 @@ @@ -93,7 +72,12 @@

파일

- + info.isFileRecursive, + (value) => setCategoryFileRecursive(info as LocalCategoryInfo, value) + } + >

하위 카테고리의 파일

@@ -109,10 +93,11 @@ info={details} onclick={({ id }) => goto(`/file/${id}?from=category`)} onRemoveClick={!details.isRecursive - ? async ({ id }) => { - await requestFileRemovalFromCategory(id, data.id as number); - void getCategoryInfo(data.id); // TODO: FIXME - } + ? ({ id }) => + tryAction( + () => removeFileFromCategory(id, data.id as number), + "카테고리에서 파일을 제거하지 못했어요.", + ) : undefined} /> {/snippet} @@ -128,13 +113,8 @@ { - if (await requestCategoryCreation(name, data.id)) { - void getCategoryInfo(data.id); // TODO: FIXME - return true; - } - return false; - }} + onCreateClick={(name: string) => + tryAction(() => createCategory(data.id, name), "카테고리를 만들지 못했어요.")} /> { - if (await requestCategoryRename(context.selectedCategory!, newName)) { - void getCategoryInfo(data.id); // TODO: FIXME - return true; - } - return false; - }} + onRenameClick={(newName: string) => + tryAction( + () => renameCategory({ ...context.selectedCategory!, parentId: data.id }, newName), + "이름을 바꾸지 못했어요.", + )} /> { - if (await requestCategoryDeletion(context.selectedCategory!)) { - void getCategoryInfo(data.id); // TODO: FIXME - return true; - } - return false; - }} + onDeleteClick={() => + tryAction( + () => deleteCategory({ ...context.selectedCategory!, parentId: data.id }), + "카테고리를 삭제하지 못했어요.", + )} /> diff --git a/src/routes/(main)/category/[[id]]/File.svelte b/src/routes/(main)/category/[[id]]/File.svelte index 363cd6d..02906b8 100644 --- a/src/routes/(main)/category/[[id]]/File.svelte +++ b/src/routes/(main)/category/[[id]]/File.svelte @@ -2,7 +2,7 @@ import { ActionEntryButton } from "#lib/components/atoms/index.js"; import { DirectoryEntryLabel } from "#lib/components/molecules/index.js"; import { getFileThumbnail } from "#lib/client/cache/index.js"; - import type { CategoryFileInfo } from "#lib/modules/filesystem/index.js"; + import type { CategoryFileInfo } from "#lib/features/filesystem/index.js"; import { formatDateTime } from "#lib/utils/index.js"; import type { SelectedFile } from "./service.svelte"; diff --git a/src/routes/(main)/category/[[id]]/service.svelte.ts b/src/routes/(main)/category/[[id]]/service.svelte.ts index 9a89c4f..7b3d105 100644 --- a/src/routes/(main)/category/[[id]]/service.svelte.ts +++ b/src/routes/(main)/category/[[id]]/service.svelte.ts @@ -1,9 +1,5 @@ import { getContext, setContext } from "svelte"; import type { SelectedCategory } from "#lib/components/molecules/index.js"; -import { encryptString } from "#lib/shared/crypto/index.js"; -import { trpc } from "#trpc/client.js"; - -export { requestCategoryCreation, requestFileRemovalFromCategory } from "#lib/services/category.js"; export interface SelectedFile { id: number; @@ -20,35 +16,3 @@ export const createContext = () => { export const useContext = () => { return getContext>("context"); }; - -export const requestCategoryRename = async (category: SelectedCategory, newName: string) => { - if (!category.dataKey) { - // TODO: Error Handling - return false; - } - - const newNameEncrypted = await encryptString(newName, category.dataKey.key); - - try { - await trpc.category.rename.mutate({ - id: category.id, - dekVersion: category.dataKey.version, - name: newNameEncrypted.ciphertext, - nameIv: newNameEncrypted.iv, - }); - return true; - } catch { - // TODO: Error Handling - return false; - } -}; - -export const requestCategoryDeletion = async (category: SelectedCategory) => { - try { - await trpc.category.delete.mutate({ id: category.id }); - return true; - } catch { - // TODO: Error Handling - return false; - } -}; diff --git a/src/routes/(main)/directory/[[id]]/+page.svelte b/src/routes/(main)/directory/[[id]]/+page.svelte index 3d01ee0..1a0c95a 100644 --- a/src/routes/(main)/directory/[[id]]/+page.svelte +++ b/src/routes/(main)/directory/[[id]]/+page.svelte @@ -3,8 +3,8 @@ import { page } from "$app/state"; import { FloatingButton } from "#lib/components/atoms/index.js"; import { TopBar } from "#lib/components/molecules/index.js"; - import { getDirectoryInfo, type MaybeDirectoryInfo } from "#lib/modules/filesystem/index.js"; - import { HybridPromise } from "#lib/utils/index.js"; + import { tryAction } from "#lib/client/toast.svelte.js"; + import { createDirectory, uploadFile, useDirectory } from "#lib/features/filesystem/index.js"; import DirectoryCreateModal from "./DirectoryCreateModal.svelte"; import DirectoryEntries from "./DirectoryEntries"; import DownloadStatusCard from "./DownloadStatusCard.svelte"; @@ -14,14 +14,7 @@ import EntryMenuBottomSheet from "./EntryMenuBottomSheet.svelte"; import EntryRenameModal from "./EntryRenameModal.svelte"; import UploadStatusCard from "./UploadStatusCard.svelte"; - import { - createContext, - requestDirectoryCreation, - requestFileUpload, - requestEntryRename, - requestEntryDeletion, - requestFavoriteToggle, - } from "./service.svelte"; + import { createContext, deleteEntry, renameEntry, toggleEntryFavorite } from "./service.svelte"; import IconSearch from "~icons/material-symbols/search"; import IconAdd from "~icons/material-symbols/add"; @@ -29,7 +22,8 @@ let { data } = $props(); let context = createContext(); - let info: MaybeDirectoryInfo | undefined = $state(); + const directory = useDirectory(() => data.id); + let info = $derived(directory.current); let fileInput: HTMLInputElement | undefined = $state(); let duplicatedFile: File | undefined = $state(); let resolveForDuplicateFileModal: ((res: boolean) => void) | undefined = $state(); @@ -56,46 +50,33 @@ await goto(`/search${query ? `?${query}` : ""}`); }; - const uploadFile = () => { + const uploadFiles = () => { const files = fileInput?.files; if (!files || files.length === 0) return; for (const file of files) { - requestFileUpload(file, data.id, () => { - return new Promise((resolve) => { - duplicatedFile = file; - resolveForDuplicateFileModal = resolve; - isDuplicateFileModalOpen = true; - }); - }) - .then((res) => { - if (!res) return; - // TODO: FIXME - void getDirectoryInfo(data.id); - }) - .catch((e: Error) => { - // TODO: FIXME - console.error(e); - }); + void tryAction( + () => + uploadFile(file, data.id, () => { + return new Promise((resolve) => { + duplicatedFile = file; + resolveForDuplicateFileModal = resolve; + isDuplicateFileModalOpen = true; + }); + }), + `'${file.name}' 파일을 업로드하지 못했어요.`, + ); } fileInput!.value = ""; }; - - $effect(() => { - HybridPromise.resolve(getDirectoryInfo(data.id)).then((result) => { - if (data.id === result.id) { - info = result; - } - }); - }); 내 파일 - +
@@ -150,13 +131,8 @@ /> { - if (await requestDirectoryCreation(name, data.id)) { - void getDirectoryInfo(data.id); // TODO: FIXME - return true; - } - return false; - }} + onCreateClick={(name) => + tryAction(() => createDirectory(data.id, name), "폴더를 만들지 못했어요.")} /> { - if (await requestFavoriteToggle(context.selectedEntry!)) { + if ( + await tryAction( + () => toggleEntryFavorite(context.selectedEntry!), + "즐겨찾기를 변경하지 못했어요.", + ) + ) { isEntryMenuBottomSheetOpen = false; - void getDirectoryInfo(data.id); // TODO: FIXME } }} /> { - if (await requestEntryRename(context.selectedEntry!, newName)) { - void getDirectoryInfo(data.id); // TODO: FIXME - return true; - } - return false; - }} + onRenameClick={(newName: string) => + tryAction(() => renameEntry(context.selectedEntry!, newName), "이름을 바꾸지 못했어요.")} /> { - if (await requestEntryDeletion(context.selectedEntry!)) { - void getDirectoryInfo(data.id); // TODO: FIXME - return true; - } - return false; - }} + onDeleteClick={() => tryAction(() => deleteEntry(context.selectedEntry!), "삭제하지 못했어요.")} /> diff --git a/src/routes/(main)/directory/[[id]]/DirectoryEntries/DirectoryEntries.svelte b/src/routes/(main)/directory/[[id]]/DirectoryEntries/DirectoryEntries.svelte index e27029d..9f522da 100644 --- a/src/routes/(main)/directory/[[id]]/DirectoryEntries/DirectoryEntries.svelte +++ b/src/routes/(main)/directory/[[id]]/DirectoryEntries/DirectoryEntries.svelte @@ -2,7 +2,7 @@ import { ActionEntryButton, RowVirtualizer } from "#lib/components/atoms/index.js"; import { DirectoryEntryLabel } from "#lib/components/molecules/index.js"; import { getUploadingFiles, type LiveFileUploadState } from "#lib/client/transfer/index.js"; - import type { DirectoryInfo } from "#lib/modules/filesystem/index.js"; + import type { DirectoryInfo } from "#lib/features/filesystem/index.js"; import { sortEntries } from "#lib/utils/index.js"; import File from "./File.svelte"; import SubDirectory from "./SubDirectory.svelte"; diff --git a/src/routes/(main)/directory/[[id]]/DirectoryEntries/File.svelte b/src/routes/(main)/directory/[[id]]/DirectoryEntries/File.svelte index 384a703..46f271c 100644 --- a/src/routes/(main)/directory/[[id]]/DirectoryEntries/File.svelte +++ b/src/routes/(main)/directory/[[id]]/DirectoryEntries/File.svelte @@ -2,7 +2,7 @@ import { ActionEntryButton } from "#lib/components/atoms/index.js"; import { DirectoryEntryLabel } from "#lib/components/molecules/index.js"; import { getFileThumbnail } from "#lib/client/cache/index.js"; - import type { SummarizedFileInfo } from "#lib/modules/filesystem/index.js"; + import type { SummarizedFileInfo } from "#lib/features/filesystem/index.js"; import { formatDateTime } from "#lib/utils/index.js"; import type { SelectedEntry } from "../service.svelte"; @@ -22,6 +22,7 @@ callback({ type: "file", id: info.id, + parentId: info.parentId, dataKey: info.dataKey, name: info.name, isFavorite: info.isFavorite, diff --git a/src/routes/(main)/directory/[[id]]/DirectoryEntries/SubDirectory.svelte b/src/routes/(main)/directory/[[id]]/DirectoryEntries/SubDirectory.svelte index 7f92440..5f5ff0e 100644 --- a/src/routes/(main)/directory/[[id]]/DirectoryEntries/SubDirectory.svelte +++ b/src/routes/(main)/directory/[[id]]/DirectoryEntries/SubDirectory.svelte @@ -1,7 +1,7 @@