diff --git a/src/lib/components/atoms/buttons/FileThumbnailButton.svelte b/src/lib/components/atoms/buttons/FileThumbnailButton.svelte
index 6c5632c..e95d07c 100644
--- a/src/lib/components/atoms/buttons/FileThumbnailButton.svelte
+++ b/src/lib/components/atoms/buttons/FileThumbnailButton.svelte
@@ -2,6 +2,8 @@
import { getFileThumbnail } from "$lib/modules/file";
import type { SummarizedFileInfo } from "$lib/modules/filesystem";
+ import IconFavorite from "~icons/material-symbols/favorite";
+
interface Props {
info: SummarizedFileInfo;
onclick?: (file: SummarizedFileInfo) => void;
@@ -14,11 +16,19 @@
diff --git a/src/lib/components/molecules/labels/DirectoryEntryLabel.svelte b/src/lib/components/molecules/labels/DirectoryEntryLabel.svelte
index e85665d..001631b 100644
--- a/src/lib/components/molecules/labels/DirectoryEntryLabel.svelte
+++ b/src/lib/components/molecules/labels/DirectoryEntryLabel.svelte
@@ -5,9 +5,11 @@
import IconFolder from "~icons/material-symbols/folder";
import IconDriveFolderUpload from "~icons/material-symbols/drive-folder-upload";
import IconDraft from "~icons/material-symbols/draft";
+ import IconFavorite from "~icons/material-symbols/favorite";
interface Props {
class?: ClassValue;
+ isFavorite?: boolean;
name: string;
subtext?: string;
textClass?: ClassValue;
@@ -17,6 +19,7 @@
let {
class: className,
+ isFavorite = false,
name,
subtext,
textClass: textClassName,
@@ -26,7 +29,7 @@
{#snippet iconSnippet()}
-
+
{#if thumbnail}

{:else if type === "directory"}
@@ -36,6 +39,14 @@
{:else}
{/if}
+ {#if isFavorite}
+
+
+
+ {/if}
{/snippet}
diff --git a/src/lib/indexedDB/filesystem.ts b/src/lib/indexedDB/filesystem.ts
index 87c0d70..d5c3e4d 100644
--- a/src/lib/indexedDB/filesystem.ts
+++ b/src/lib/indexedDB/filesystem.ts
@@ -4,6 +4,7 @@ interface DirectoryInfo {
id: number;
parentId: DirectoryId;
name: string;
+ isFavorite?: boolean;
}
interface FileInfo {
@@ -14,6 +15,7 @@ interface FileInfo {
createdAt?: Date;
lastModifiedAt: Date;
categoryIds?: number[];
+ isFavorite?: boolean;
}
interface CategoryInfo {
diff --git a/src/lib/modules/filesystem/category.ts b/src/lib/modules/filesystem/category.ts
index 94740c6..db0ec45 100644
--- a/src/lib/modules/filesystem/category.ts
+++ b/src/lib/modules/filesystem/category.ts
@@ -22,6 +22,7 @@ const cache = new FilesystemCache
({
name: fileInfo.name,
createdAt: fileInfo.createdAt,
lastModifiedAt: fileInfo.lastModifiedAt,
+ isFavorite: fileInfo.isFavorite,
isRecursive: file.isRecursive,
}
: undefined;
@@ -66,6 +67,7 @@ const cache = new FilesystemCache({
parentId: file.parent,
contentType: file.contentType,
isRecursive: file.isRecursive,
+ isFavorite: file.isFavorite,
...(await decryptFileMetadata(file, masterKey)),
})),
),
diff --git a/src/lib/modules/filesystem/file.ts b/src/lib/modules/filesystem/file.ts
index 5ef0fae..2517c02 100644
--- a/src/lib/modules/filesystem/file.ts
+++ b/src/lib/modules/filesystem/file.ts
@@ -27,6 +27,7 @@ const cache = new FilesystemCache({
name: file.name,
createdAt: file.createdAt,
lastModifiedAt: file.lastModifiedAt,
+ isFavorite: file.isFavorite,
categories: categories?.filter((category) => !!category) ?? [],
};
}
@@ -55,6 +56,7 @@ const cache = new FilesystemCache({
name: metadata.name,
createdAt: metadata.createdAt,
lastModifiedAt: metadata.lastModifiedAt,
+ isFavorite: file.isFavorite,
categories,
});
} catch (e) {
@@ -121,6 +123,7 @@ const cache = new FilesystemCache({
parentId: metadataRaw.parent,
contentType: metadataRaw.contentType,
categories,
+ isFavorite: metadataRaw.isFavorite,
...metadata,
};
}),
diff --git a/src/lib/server/db/file.ts b/src/lib/server/db/file.ts
index d70abf8..b6967be 100644
--- a/src/lib/server/db/file.ts
+++ b/src/lib/server/db/file.ts
@@ -641,6 +641,7 @@ export const searchFiles = async (
encName: file.encrypted_name,
encCreatedAt: file.encrypted_created_at,
encLastModifiedAt: file.encrypted_last_modified_at,
+ isFavorite: file.is_favorite,
}));
};
diff --git a/src/routes/(fullscreen)/file/[id]/+page.svelte b/src/routes/(fullscreen)/file/[id]/+page.svelte
index 9ae8825..5922410 100644
--- a/src/routes/(fullscreen)/file/[id]/+page.svelte
+++ b/src/routes/(fullscreen)/file/[id]/+page.svelte
@@ -18,6 +18,7 @@
requestThumbnailUpload,
requestFileAdditionToCategory,
requestVideoStream,
+ requestFavoriteToggle,
} from "./service";
import TopBarMenu from "./TopBarMenu.svelte";
@@ -75,6 +76,15 @@
void getFileInfo(data.id, $masterKeyStore?.get(1)?.key!); // TODO: FIXME
};
+ const toggleFavorite = async () => {
+ if (!info?.exists) return;
+ const isFavorite = !!info.isFavorite;
+ const success = await requestFavoriteToggle(data.id, isFavorite);
+ if (success) {
+ info.isFavorite = !isFavorite;
+ }
+ };
+
$effect(() => {
HybridPromise.resolve(getFileInfo(data.id, $masterKeyStore?.get(1)?.key!)).then((result) => {
if (data.id === result.id) {
@@ -158,6 +168,8 @@
{fileBlob}
downloadUrl={videoStreamUrl}
filename={info?.name}
+ isFavorite={info?.isFavorite}
+ onToggleFavorite={toggleFavorite}
/>
diff --git a/src/routes/(fullscreen)/file/[id]/TopBarMenu.svelte b/src/routes/(fullscreen)/file/[id]/TopBarMenu.svelte
index d713e8c..923cff9 100644
--- a/src/routes/(fullscreen)/file/[id]/TopBarMenu.svelte
+++ b/src/routes/(fullscreen)/file/[id]/TopBarMenu.svelte
@@ -5,6 +5,8 @@
import { fly } from "svelte/transition";
import { goto } from "$app/navigation";
+ import IconFavorite from "~icons/material-symbols/favorite";
+ import IconFavoriteOutline from "~icons/material-symbols/favorite-outline";
import IconFolderOpen from "~icons/material-symbols/folder-open";
import IconCloudDownload from "~icons/material-symbols/cloud-download";
@@ -13,10 +15,20 @@
downloadUrl?: string;
fileBlob?: Blob;
filename?: string;
+ isFavorite?: boolean;
isOpen: boolean;
+ onToggleFavorite?: () => void;
}
- let { directoryId, downloadUrl, fileBlob, filename, isOpen = $bindable() }: Props = $props();
+ let {
+ directoryId,
+ downloadUrl,
+ fileBlob,
+ filename,
+ isFavorite,
+ isOpen = $bindable(),
+ onToggleFavorite,
+ }: Props = $props();
const handleDownload = () => {
if (fileBlob && filename) {
@@ -34,7 +46,7 @@
{#if isOpen && (directoryId || downloadUrl || fileBlob)}
더보기
@@ -54,6 +66,13 @@
{/snippet}
+ {#if typeof isFavorite === "boolean"}
+ {@render menuButton(
+ isFavorite ? IconFavorite : IconFavoriteOutline,
+ isFavorite ? "즐겨찾기 해제" : "즐겨찾기",
+ onToggleFavorite ?? (() => {}),
+ )}
+ {/if}
{#if directoryId}
{@render menuButton(IconFolderOpen, "폴더에서 보기", () =>
goto(
diff --git a/src/routes/(fullscreen)/file/[id]/service.ts b/src/routes/(fullscreen)/file/[id]/service.ts
index 598418b..09b7d28 100644
--- a/src/routes/(fullscreen)/file/[id]/service.ts
+++ b/src/routes/(fullscreen)/file/[id]/service.ts
@@ -48,3 +48,16 @@ export const requestFileAdditionToCategory = async (fileId: number, categoryId:
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 {
+ return false;
+ }
+};
diff --git a/src/routes/(fullscreen)/search/Directory.svelte b/src/routes/(fullscreen)/search/Directory.svelte
index 56448a6..700bd54 100644
--- a/src/routes/(fullscreen)/search/Directory.svelte
+++ b/src/routes/(fullscreen)/search/Directory.svelte
@@ -12,5 +12,5 @@
-
+
diff --git a/src/routes/(fullscreen)/search/File.svelte b/src/routes/(fullscreen)/search/File.svelte
index 3db8915..4e5c631 100644
--- a/src/routes/(fullscreen)/search/File.svelte
+++ b/src/routes/(fullscreen)/search/File.svelte
@@ -21,5 +21,6 @@
thumbnail={$thumbnail}
name={info.name}
subtext={formatDateTime(info.createdAt ?? info.lastModifiedAt)}
+ isFavorite={info.isFavorite}
/>
diff --git a/src/routes/(fullscreen)/search/service.ts b/src/routes/(fullscreen)/search/service.ts
index d12ea1a..3f250dc 100644
--- a/src/routes/(fullscreen)/search/service.ts
+++ b/src/routes/(fullscreen)/search/service.ts
@@ -46,6 +46,7 @@ export const requestSearch = async (filter: SearchFilter, masterKey: CryptoKey)
exists: true,
parentId: directory.parent,
...metadata,
+ isFavorite: !!directory.isFavorite,
};
},
}),
@@ -65,6 +66,7 @@ export const requestSearch = async (filter: SearchFilter, masterKey: CryptoKey)
exists: true,
parentId: file.parent,
contentType: file.contentType,
+ isFavorite: !!file.isFavorite,
...metadata,
};
},
diff --git a/src/routes/(main)/category/[[id]]/File.svelte b/src/routes/(main)/category/[[id]]/File.svelte
index 06ed8b3..90fb659 100644
--- a/src/routes/(main)/category/[[id]]/File.svelte
+++ b/src/routes/(main)/category/[[id]]/File.svelte
@@ -24,5 +24,10 @@
actionButtonIcon={onRemoveClick && IconClose}
onActionButtonClick={() => onRemoveClick?.(info)}
>
-
+
diff --git a/src/routes/(main)/directory/[[id]]/DirectoryEntries/File.svelte b/src/routes/(main)/directory/[[id]]/DirectoryEntries/File.svelte
index a6c30c4..d45beb7 100644
--- a/src/routes/(main)/directory/[[id]]/DirectoryEntries/File.svelte
+++ b/src/routes/(main)/directory/[[id]]/DirectoryEntries/File.svelte
@@ -40,5 +40,6 @@
thumbnail={$thumbnail}
name={info.name}
subtext={formatDateTime(info.createdAt ?? info.lastModifiedAt)}
+ isFavorite={info.isFavorite}
/>
diff --git a/src/routes/(main)/directory/[[id]]/DirectoryEntries/SubDirectory.svelte b/src/routes/(main)/directory/[[id]]/DirectoryEntries/SubDirectory.svelte
index 3f86025..33f4a47 100644
--- a/src/routes/(main)/directory/[[id]]/DirectoryEntries/SubDirectory.svelte
+++ b/src/routes/(main)/directory/[[id]]/DirectoryEntries/SubDirectory.svelte
@@ -31,5 +31,5 @@
actionButtonIcon={IconMoreVert}
onActionButtonClick={() => action(onOpenMenuClick)}
>
-
+
diff --git a/src/trpc/routers/category.ts b/src/trpc/routers/category.ts
index 34887f7..1d4fcee 100644
--- a/src/trpc/routers/category.ts
+++ b/src/trpc/routers/category.ts
@@ -57,6 +57,7 @@ const categoryRouter = router({
createdAtIv: file.encCreatedAt?.iv,
lastModifiedAt: file.encLastModifiedAt.ciphertext,
lastModifiedAtIv: file.encLastModifiedAt.iv,
+ isFavorite: file.isFavorite,
isRecursive: file.isRecursive,
})),
};
diff --git a/src/trpc/routers/file.ts b/src/trpc/routers/file.ts
index ce6e5a6..ad06cdc 100644
--- a/src/trpc/routers/file.ts
+++ b/src/trpc/routers/file.ts
@@ -31,6 +31,7 @@ const fileRouter = router({
createdAtIv: file.encCreatedAt?.iv,
lastModifiedAt: file.encLastModifiedAt.ciphertext,
lastModifiedAtIv: file.encLastModifiedAt.iv,
+ isFavorite: file.isFavorite,
categories: categories.map((category) => ({
id: category.id,
parent: category.parentId,
@@ -65,6 +66,7 @@ const fileRouter = router({
createdAtIv: file.encCreatedAt?.iv,
lastModifiedAt: file.encLastModifiedAt.ciphertext,
lastModifiedAtIv: file.encLastModifiedAt.iv,
+ isFavorite: file.isFavorite,
categories: file.categories.map((category) => ({
id: category.id,
parent: category.parentId,
diff --git a/src/trpc/routers/search.ts b/src/trpc/routers/search.ts
index 5aa2820..34e814d 100644
--- a/src/trpc/routers/search.ts
+++ b/src/trpc/routers/search.ts
@@ -32,6 +32,7 @@ const searchRouter = router({
dekVersion: directory.dekVersion,
name: directory.encName.ciphertext,
nameIv: directory.encName.iv,
+ isFavorite: directory.isFavorite,
})),
files: files.map((file) => ({
id: file.id,
@@ -46,6 +47,7 @@ const searchRouter = router({
createdAtIv: file.encCreatedAt?.iv,
lastModifiedAt: file.encLastModifiedAt.ciphertext,
lastModifiedAtIv: file.encLastModifiedAt.iv,
+ isFavorite: file.isFavorite,
})),
};
}),