mirror of
https://github.com/kmc7468/arkvault.git
synced 2026-11-04 08:10:28 +09:00
파일 시스템 관련 상태와 변경 로직을 도메인 모듈로 통합하여 캐시가 자동으로 갱신되도록 개선하고 오류를 토스트로 표시하도록 변경
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
2ce73aa331
commit
4e82c5e556
67 files changed
+1091
-1082
No files matched your search
@@ -3,36 +3,36 @@
|
||||
import { CheckBox, RowVirtualizer } from "#lib/components/atoms/index.js";
|
||||
import { SubCategories, TopBar } from "#lib/components/molecules/index.js";
|
||||
import { CategoryCreateModal } from "#lib/components/organisms/index.js";
|
||||
import { updateCategoryInfo } from "#lib/client/storage/indexedDB/index.js";
|
||||
import { getCategoryInfo, type MaybeCategoryInfo } from "#lib/modules/filesystem/index.js";
|
||||
import { HybridPromise, sortEntries } from "#lib/utils/index.js";
|
||||
import { tryAction } from "#lib/client/toast.svelte.js";
|
||||
import {
|
||||
createCategory,
|
||||
deleteCategory,
|
||||
removeFileFromCategory,
|
||||
renameCategory,
|
||||
setCategoryFileRecursive,
|
||||
useCategory,
|
||||
type LocalCategoryInfo,
|
||||
} from "#lib/features/filesystem/index.js";
|
||||
import { sortEntries } from "#lib/utils/index.js";
|
||||
import CategoryDeleteModal from "./CategoryDeleteModal.svelte";
|
||||
import CategoryMenuBottomSheet from "./CategoryMenuBottomSheet.svelte";
|
||||
import CategoryRenameModal from "./CategoryRenameModal.svelte";
|
||||
import File from "./File.svelte";
|
||||
import {
|
||||
createContext,
|
||||
requestCategoryCreation,
|
||||
requestFileRemovalFromCategory,
|
||||
requestCategoryRename,
|
||||
requestCategoryDeletion,
|
||||
} from "./service.svelte";
|
||||
import { createContext } from "./service.svelte";
|
||||
|
||||
import IconMoreVert from "~icons/material-symbols/more-vert";
|
||||
|
||||
let { data } = $props();
|
||||
let context = createContext();
|
||||
|
||||
let info: MaybeCategoryInfo | undefined = $state();
|
||||
const category = useCategory(() => data.id);
|
||||
let info = $derived(category.current);
|
||||
|
||||
let isCategoryCreateModalOpen = $state(false);
|
||||
let isCategoryMenuBottomSheetOpen = $state(false);
|
||||
let isCategoryRenameModalOpen = $state(false);
|
||||
let isCategoryDeleteModalOpen = $state(false);
|
||||
|
||||
let lastCategoryId: CategoryId | undefined = $state();
|
||||
let lastIsFileRecursive: boolean | undefined = $state();
|
||||
|
||||
let files = $derived(
|
||||
sortEntries(
|
||||
info?.files
|
||||
@@ -40,27 +40,6 @@
|
||||
.filter(({ details }) => info?.isFileRecursive || !details.isRecursive) ?? [],
|
||||
),
|
||||
);
|
||||
|
||||
$effect(() => {
|
||||
if (!info || info.id === "root" || info.isFileRecursive === undefined) return;
|
||||
if (lastCategoryId !== info.id) {
|
||||
lastCategoryId = info.id;
|
||||
lastIsFileRecursive = info.isFileRecursive;
|
||||
return;
|
||||
}
|
||||
if (lastIsFileRecursive === info.isFileRecursive) return;
|
||||
|
||||
lastIsFileRecursive = info.isFileRecursive;
|
||||
void updateCategoryInfo(info.id, { isFileRecursive: info.isFileRecursive });
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
HybridPromise.resolve(getCategoryInfo(data.id)).then((result) => {
|
||||
if (data.id === result.id) {
|
||||
info = result;
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
@@ -93,7 +72,12 @@
|
||||
<div class="space-y-2 bg-white p-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<p class="text-lg font-bold text-gray-800">파일</p>
|
||||
<CheckBox bind:checked={info.isFileRecursive}>
|
||||
<CheckBox
|
||||
bind:checked={
|
||||
() => info.isFileRecursive,
|
||||
(value) => setCategoryFileRecursive(info as LocalCategoryInfo, value)
|
||||
}
|
||||
>
|
||||
<p class="font-medium">하위 카테고리의 파일</p>
|
||||
</CheckBox>
|
||||
</div>
|
||||
@@ -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 @@
|
||||
|
||||
<CategoryCreateModal
|
||||
bind:isOpen={isCategoryCreateModalOpen}
|
||||
onCreateClick={async (name: string) => {
|
||||
if (await requestCategoryCreation(name, data.id)) {
|
||||
void getCategoryInfo(data.id); // TODO: FIXME
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}}
|
||||
onCreateClick={(name: string) =>
|
||||
tryAction(() => createCategory(data.id, name), "카테고리를 만들지 못했어요.")}
|
||||
/>
|
||||
|
||||
<CategoryMenuBottomSheet
|
||||
@@ -150,21 +130,17 @@
|
||||
/>
|
||||
<CategoryRenameModal
|
||||
bind:isOpen={isCategoryRenameModalOpen}
|
||||
onRenameClick={async (newName: string) => {
|
||||
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),
|
||||
"이름을 바꾸지 못했어요.",
|
||||
)}
|
||||
/>
|
||||
<CategoryDeleteModal
|
||||
bind:isOpen={isCategoryDeleteModalOpen}
|
||||
onDeleteClick={async () => {
|
||||
if (await requestCategoryDeletion(context.selectedCategory!)) {
|
||||
void getCategoryInfo(data.id); // TODO: FIXME
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}}
|
||||
onDeleteClick={() =>
|
||||
tryAction(
|
||||
() => deleteCategory({ ...context.selectedCategory!, parentId: data.id }),
|
||||
"카테고리를 삭제하지 못했어요.",
|
||||
)}
|
||||
/>
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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<ReturnType<typeof createContext>>("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;
|
||||
}
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>내 파일</title>
|
||||
</svelte:head>
|
||||
|
||||
<input bind:this={fileInput} onchange={uploadFile} type="file" multiple class="hidden" />
|
||||
<input bind:this={fileInput} onchange={uploadFiles} type="file" multiple class="hidden" />
|
||||
|
||||
<div class="flex h-full flex-col">
|
||||
<TopBar title={info?.name ?? "내 파일"} {showBackButton} class="shrink-0">
|
||||
@@ -150,13 +131,8 @@
|
||||
/>
|
||||
<DirectoryCreateModal
|
||||
bind:isOpen={isDirectoryCreateModalOpen}
|
||||
onCreateClick={async (name) => {
|
||||
if (await requestDirectoryCreation(name, data.id)) {
|
||||
void getDirectoryInfo(data.id); // TODO: FIXME
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}}
|
||||
onCreateClick={(name) =>
|
||||
tryAction(() => createDirectory(data.id, name), "폴더를 만들지 못했어요.")}
|
||||
/>
|
||||
<DuplicateFileModal
|
||||
bind:isOpen={isDuplicateFileModalOpen}
|
||||
@@ -182,29 +158,22 @@
|
||||
isEntryDeleteModalOpen = true;
|
||||
}}
|
||||
onFavoriteClick={async () => {
|
||||
if (await requestFavoriteToggle(context.selectedEntry!)) {
|
||||
if (
|
||||
await tryAction(
|
||||
() => toggleEntryFavorite(context.selectedEntry!),
|
||||
"즐겨찾기를 변경하지 못했어요.",
|
||||
)
|
||||
) {
|
||||
isEntryMenuBottomSheetOpen = false;
|
||||
void getDirectoryInfo(data.id); // TODO: FIXME
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<EntryRenameModal
|
||||
bind:isOpen={isEntryRenameModalOpen}
|
||||
onRenameClick={async (newName: string) => {
|
||||
if (await requestEntryRename(context.selectedEntry!, newName)) {
|
||||
void getDirectoryInfo(data.id); // TODO: FIXME
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}}
|
||||
onRenameClick={(newName: string) =>
|
||||
tryAction(() => renameEntry(context.selectedEntry!, newName), "이름을 바꾸지 못했어요.")}
|
||||
/>
|
||||
<EntryDeleteModal
|
||||
bind:isOpen={isEntryDeleteModalOpen}
|
||||
onDeleteClick={async () => {
|
||||
if (await requestEntryDeletion(context.selectedEntry!)) {
|
||||
void getDirectoryInfo(data.id); // TODO: FIXME
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}}
|
||||
onDeleteClick={() => tryAction(() => deleteEntry(context.selectedEntry!), "삭제하지 못했어요.")}
|
||||
/>
|
||||
@@ -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";
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { ActionEntryButton } from "#lib/components/atoms/index.js";
|
||||
import { DirectoryEntryLabel } from "#lib/components/molecules/index.js";
|
||||
import type { SubDirectoryInfo } from "#lib/modules/filesystem/index.js";
|
||||
import type { SubDirectoryInfo } from "#lib/features/filesystem/index.js";
|
||||
import type { SelectedEntry } from "../service.svelte";
|
||||
|
||||
import IconMoreVert from "~icons/material-symbols/more-vert";
|
||||
@@ -18,6 +18,7 @@
|
||||
callback({
|
||||
type: "directory",
|
||||
id: info.id,
|
||||
parentId: info.parentId,
|
||||
dataKey: info.dataKey,
|
||||
name: info.name,
|
||||
isFavorite: info.isFavorite,
|
||||
|
||||
@@ -1,19 +1,18 @@
|
||||
import { getContext, setContext } from "svelte";
|
||||
import {
|
||||
storeFileCache,
|
||||
deleteFileCache,
|
||||
storeFileThumbnailCache,
|
||||
deleteFileThumbnailCache,
|
||||
} from "#lib/client/cache/index.js";
|
||||
import { keyring } from "#lib/client/keys/index.js";
|
||||
import { uploadFile } from "#lib/client/transfer/index.js";
|
||||
import type { DataKey } from "#lib/modules/filesystem/index.js";
|
||||
import { generateDataKey, wrapDataKey, encryptString } from "#lib/shared/crypto/index.js";
|
||||
import { trpc } from "#trpc/client.js";
|
||||
deleteDirectory,
|
||||
deleteFile,
|
||||
renameDirectory,
|
||||
renameFile,
|
||||
setDirectoryFavorite,
|
||||
setFileFavorite,
|
||||
type DataKey,
|
||||
} from "#lib/features/filesystem/index.js";
|
||||
|
||||
export interface SelectedEntry {
|
||||
type: "directory" | "file";
|
||||
id: number;
|
||||
parentId: DirectoryId;
|
||||
dataKey: DataKey | undefined;
|
||||
name: string;
|
||||
isFavorite: boolean;
|
||||
@@ -30,115 +29,26 @@ export const useContext = () => {
|
||||
return getContext<ReturnType<typeof createContext>>("context");
|
||||
};
|
||||
|
||||
export const requestDirectoryCreation = async (name: string, parentId: "root" | number) => {
|
||||
const masterKey = keyring.activeMasterKey;
|
||||
const { dataKey, dataKeyVersion } = await generateDataKey();
|
||||
const nameEncrypted = await encryptString(name, dataKey);
|
||||
|
||||
try {
|
||||
await trpc.directory.create.mutate({
|
||||
parent: parentId,
|
||||
mekVersion: masterKey.version,
|
||||
dek: await wrapDataKey(dataKey, masterKey.key),
|
||||
dekVersion: dataKeyVersion,
|
||||
name: nameEncrypted.ciphertext,
|
||||
nameIv: nameEncrypted.iv,
|
||||
});
|
||||
return true;
|
||||
} catch {
|
||||
// TODO: Error Handling
|
||||
return false;
|
||||
export const renameEntry = async (entry: SelectedEntry, newName: string) => {
|
||||
if (entry.type === "directory") {
|
||||
await renameDirectory(entry, newName);
|
||||
} else {
|
||||
await renameFile(entry, newName);
|
||||
}
|
||||
};
|
||||
|
||||
export const requestFileUpload = async (
|
||||
file: File,
|
||||
parentId: "root" | number,
|
||||
onDuplicate: () => Promise<boolean>,
|
||||
) => {
|
||||
const res = await uploadFile(file, parentId, onDuplicate);
|
||||
if (!res) return false;
|
||||
|
||||
if (res.fileBuffer) {
|
||||
storeFileCache(res.fileId, res.fileBuffer); // Intended
|
||||
}
|
||||
if (res.thumbnailBuffer) {
|
||||
storeFileThumbnailCache(res.fileId, res.thumbnailBuffer); // Intended
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
export const requestEntryRename = async (entry: SelectedEntry, newName: string) => {
|
||||
if (!entry.dataKey) {
|
||||
// TODO: Error Handling
|
||||
return false;
|
||||
}
|
||||
|
||||
const newNameEncrypted = await encryptString(newName, entry.dataKey.key);
|
||||
|
||||
try {
|
||||
if (entry.type === "directory") {
|
||||
await trpc.directory.rename.mutate({
|
||||
id: entry.id,
|
||||
dekVersion: entry.dataKey.version,
|
||||
name: newNameEncrypted.ciphertext,
|
||||
nameIv: newNameEncrypted.iv,
|
||||
});
|
||||
} else {
|
||||
await trpc.file.rename.mutate({
|
||||
id: entry.id,
|
||||
dekVersion: entry.dataKey.version,
|
||||
name: newNameEncrypted.ciphertext,
|
||||
nameIv: newNameEncrypted.iv,
|
||||
});
|
||||
}
|
||||
return true;
|
||||
} catch {
|
||||
// TODO: Error Handling
|
||||
return false;
|
||||
export const deleteEntry = async (entry: SelectedEntry) => {
|
||||
if (entry.type === "directory") {
|
||||
await deleteDirectory(entry);
|
||||
} else {
|
||||
await deleteFile(entry);
|
||||
}
|
||||
};
|
||||
|
||||
export const requestEntryDeletion = async (entry: SelectedEntry) => {
|
||||
try {
|
||||
if (entry.type === "directory") {
|
||||
const { deletedFiles } = await trpc.directory.delete.mutate({ id: entry.id });
|
||||
await Promise.all(
|
||||
deletedFiles.flatMap((fileId) => [
|
||||
deleteFileCache(fileId),
|
||||
deleteFileThumbnailCache(fileId),
|
||||
]),
|
||||
);
|
||||
} else {
|
||||
await trpc.file.delete.mutate({ id: entry.id });
|
||||
await Promise.all([deleteFileCache(entry.id), deleteFileThumbnailCache(entry.id)]);
|
||||
}
|
||||
return true;
|
||||
} catch {
|
||||
// TODO: Error Handling
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
export const requestFavoriteToggle = async (entry: SelectedEntry) => {
|
||||
try {
|
||||
if (entry.type === "directory") {
|
||||
if (entry.isFavorite) {
|
||||
await trpc.favorites.removeDirectory.mutate({ id: entry.id });
|
||||
} else {
|
||||
await trpc.favorites.addDirectory.mutate({ id: entry.id });
|
||||
}
|
||||
} else {
|
||||
if (entry.isFavorite) {
|
||||
await trpc.favorites.removeFile.mutate({ id: entry.id });
|
||||
} else {
|
||||
await trpc.favorites.addFile.mutate({ id: entry.id });
|
||||
}
|
||||
}
|
||||
return true;
|
||||
} catch {
|
||||
// TODO: Error Handling
|
||||
return false;
|
||||
export const toggleEntryFavorite = async (entry: SelectedEntry) => {
|
||||
if (entry.type === "directory") {
|
||||
await setDirectoryFavorite(entry, !entry.isFavorite);
|
||||
} else {
|
||||
await setFileFavorite(entry, !entry.isFavorite);
|
||||
}
|
||||
};
|
||||
@@ -5,22 +5,30 @@
|
||||
import { TopBar } from "#lib/components/molecules/index.js";
|
||||
import Directory from "./Directory.svelte";
|
||||
import File from "./File.svelte";
|
||||
import { requestFavoriteEntries, requestRemoveFavorite, type FavoriteEntry } from "./service";
|
||||
import { showErrorToast, tryAction } from "#lib/client/toast.svelte.js";
|
||||
import {
|
||||
getFavoriteEntries,
|
||||
removeFavorite,
|
||||
type FavoriteEntry,
|
||||
} from "#lib/features/favorites/index.js";
|
||||
|
||||
import IconSearch from "~icons/material-symbols/search";
|
||||
|
||||
let { data } = $props();
|
||||
|
||||
let entries: FavoriteEntry[] = $state([]);
|
||||
let isLoading = $state(true);
|
||||
|
||||
onMount(async () => {
|
||||
entries = await requestFavoriteEntries(data.favorites);
|
||||
isLoading = false;
|
||||
try {
|
||||
entries = await getFavoriteEntries();
|
||||
} catch (e) {
|
||||
showErrorToast(e, "즐겨찾기를 불러오지 못했어요.");
|
||||
} finally {
|
||||
isLoading = false;
|
||||
}
|
||||
});
|
||||
|
||||
const handleRemove = async (entry: FavoriteEntry) => {
|
||||
if (await requestRemoveFavorite(entry.type, entry.details.id)) {
|
||||
if (await tryAction(() => removeFavorite(entry), "즐겨찾기를 해제하지 못했어요.")) {
|
||||
entries = entries.filter(
|
||||
(e) => !(e.type === entry.type && e.details.id === entry.details.id),
|
||||
);
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
import { trpc } from "#trpc/client.js";
|
||||
import type { PageLoad } from "./$types";
|
||||
|
||||
export const load: PageLoad = async ({ parent }) => {
|
||||
await parent(); // Wait for the key guard in the root layout
|
||||
const favorites = await trpc.favorites.get.query();
|
||||
return { favorites };
|
||||
};
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { ActionEntryButton } from "#lib/components/atoms/index.js";
|
||||
import { DirectoryEntryLabel } from "#lib/components/molecules/index.js";
|
||||
import type { SubDirectoryInfo } from "#lib/modules/filesystem/index.js";
|
||||
import type { SubDirectoryInfo } from "#lib/features/filesystem/index.js";
|
||||
|
||||
import IconClose from "~icons/material-symbols/close";
|
||||
|
||||
|
||||
@@ -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 IconClose from "~icons/material-symbols/close";
|
||||
|
||||
@@ -1,61 +0,0 @@
|
||||
import {
|
||||
getDirectoryInfo,
|
||||
getFileInfo,
|
||||
type SummarizedFileInfo,
|
||||
type SubDirectoryInfo,
|
||||
type LocalDirectoryInfo,
|
||||
} from "#lib/modules/filesystem/index.js";
|
||||
import { HybridPromise, sortEntries } from "#lib/utils/index.js";
|
||||
import { trpc } from "#trpc/client.js";
|
||||
import type { RouterOutputs } from "#trpc/router.server.js";
|
||||
|
||||
export type FavoriteEntry =
|
||||
| { type: "directory"; name: string; details: SubDirectoryInfo }
|
||||
| { type: "file"; name: string; details: SummarizedFileInfo };
|
||||
|
||||
export const requestFavoriteEntries = async (favorites: RouterOutputs["favorites"]["get"]) => {
|
||||
const [directories, files] = await HybridPromise.all([
|
||||
HybridPromise.all(
|
||||
favorites.directories.map((directory) =>
|
||||
getDirectoryInfo(directory.id, {
|
||||
serverResponse: { ...directory, isFavorite: true },
|
||||
}),
|
||||
),
|
||||
),
|
||||
HybridPromise.all(
|
||||
favorites.files.map((file) =>
|
||||
getFileInfo(file.id, { serverResponse: { ...file, isFavorite: true } }),
|
||||
),
|
||||
),
|
||||
]);
|
||||
return [
|
||||
...sortEntries(
|
||||
directories.map((directory): FavoriteEntry => ({
|
||||
type: "directory",
|
||||
name: directory.name!,
|
||||
details: directory as LocalDirectoryInfo,
|
||||
})),
|
||||
),
|
||||
...sortEntries(
|
||||
files.map((file): FavoriteEntry => ({
|
||||
type: "file",
|
||||
name: file.name!,
|
||||
details: file as SummarizedFileInfo,
|
||||
})),
|
||||
),
|
||||
];
|
||||
};
|
||||
|
||||
export const requestRemoveFavorite = async (type: "file" | "directory", id: number) => {
|
||||
try {
|
||||
if (type === "directory") {
|
||||
await trpc.favorites.removeDirectory.mutate({ id });
|
||||
} else {
|
||||
await trpc.favorites.removeFile.mutate({ id });
|
||||
}
|
||||
return true;
|
||||
} catch {
|
||||
// TODO: Error Handling
|
||||
return false;
|
||||
}
|
||||
};
|
||||
@@ -2,7 +2,7 @@
|
||||
import { onMount } from "svelte";
|
||||
import { goto } from "$app/navigation";
|
||||
import { EntryButton, FileThumbnailButton } from "#lib/components/atoms/index.js";
|
||||
import { type SummarizedFileInfo } from "#lib/modules/filesystem/index.js";
|
||||
import { type SummarizedFileInfo } from "#lib/features/filesystem/index.js";
|
||||
import { requestFreshMediaFilesRetrieval } from "./service";
|
||||
|
||||
let mediaFiles: SummarizedFileInfo[] = $state([]);
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
<script lang="ts">
|
||||
import { goto } from "$app/navigation";
|
||||
import MenuEntryButton from "./MenuEntryButton.svelte";
|
||||
import { requestLogout } from "./service";
|
||||
import { tryAction } from "#lib/client/toast.svelte.js";
|
||||
import { logout as requestLogout } from "#lib/features/auth/index.js";
|
||||
|
||||
import IconStorage from "~icons/material-symbols/storage";
|
||||
import IconImage from "~icons/material-symbols/image";
|
||||
@@ -11,7 +12,7 @@
|
||||
let { data } = $props();
|
||||
|
||||
const logout = async () => {
|
||||
if (await requestLogout()) {
|
||||
if (await tryAction(requestLogout, "로그아웃하지 못했어요.")) {
|
||||
await goto("/auth/login");
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
export { requestLogout } from "#lib/services/auth.js";
|
||||
Reference in new issue
Block a user