파일 페이지에서 즐겨찾기 설정이 가능하도록 변경 및 즐겨찾기에 추가된 경우 목록에서 즐겨찾기 여부를 아이콘으로 표시하도록 개선

This commit is contained in:
static
2026-01-17 20:11:01 +09:00
parent 420e30f677
commit ff6ea3a0b9
18 changed files with 94 additions and 7 deletions

View File

@@ -2,6 +2,8 @@
import { getFileThumbnail } from "$lib/modules/file"; import { getFileThumbnail } from "$lib/modules/file";
import type { SummarizedFileInfo } from "$lib/modules/filesystem"; import type { SummarizedFileInfo } from "$lib/modules/filesystem";
import IconFavorite from "~icons/material-symbols/favorite";
interface Props { interface Props {
info: SummarizedFileInfo; info: SummarizedFileInfo;
onclick?: (file: SummarizedFileInfo) => void; onclick?: (file: SummarizedFileInfo) => void;
@@ -14,11 +16,19 @@
<button <button
onclick={onclick && (() => setTimeout(() => onclick(info), 100))} onclick={onclick && (() => setTimeout(() => onclick(info), 100))}
class="aspect-square overflow-hidden rounded transition active:scale-95 active:brightness-90" class="relative aspect-square overflow-hidden rounded transition active:scale-95 active:brightness-90"
> >
{#if $thumbnail} {#if $thumbnail}
<img src={$thumbnail} alt={info.name} class="h-full w-full object-cover" /> <img src={$thumbnail} alt={info.name} class="h-full w-full object-cover" />
{:else} {:else}
<div class="h-full w-full bg-gray-100"></div> <div class="h-full w-full bg-gray-100"></div>
{/if} {/if}
{#if info.isFavorite}
<div class={["absolute bottom-0 right-0", !thumbnail && "rounded-full bg-white p-0.5"]}>
<IconFavorite
class="text-sm text-red-500"
style="filter: drop-shadow(0 0 1px white) drop-shadow(0 0 1px white);"
/>
</div>
{/if}
</button> </button>

View File

@@ -5,9 +5,11 @@
import IconFolder from "~icons/material-symbols/folder"; import IconFolder from "~icons/material-symbols/folder";
import IconDriveFolderUpload from "~icons/material-symbols/drive-folder-upload"; import IconDriveFolderUpload from "~icons/material-symbols/drive-folder-upload";
import IconDraft from "~icons/material-symbols/draft"; import IconDraft from "~icons/material-symbols/draft";
import IconFavorite from "~icons/material-symbols/favorite";
interface Props { interface Props {
class?: ClassValue; class?: ClassValue;
isFavorite?: boolean;
name: string; name: string;
subtext?: string; subtext?: string;
textClass?: ClassValue; textClass?: ClassValue;
@@ -17,6 +19,7 @@
let { let {
class: className, class: className,
isFavorite = false,
name, name,
subtext, subtext,
textClass: textClassName, textClass: textClassName,
@@ -26,7 +29,7 @@
</script> </script>
{#snippet iconSnippet()} {#snippet iconSnippet()}
<div class="flex h-10 w-10 items-center justify-center text-xl"> <div class="relative flex h-10 w-10 items-center justify-center text-xl">
{#if thumbnail} {#if thumbnail}
<img src={thumbnail} alt={name} loading="lazy" class="aspect-square rounded object-cover" /> <img src={thumbnail} alt={name} loading="lazy" class="aspect-square rounded object-cover" />
{:else if type === "directory"} {:else if type === "directory"}
@@ -36,6 +39,14 @@
{:else} {:else}
<IconDraft class="text-blue-400" /> <IconDraft class="text-blue-400" />
{/if} {/if}
{#if isFavorite}
<div class={["absolute bottom-0 right-0", !thumbnail && "rounded-full bg-white p-0.5"]}>
<IconFavorite
class="text-xs text-red-500"
style="filter: drop-shadow(0 0 1px white) drop-shadow(0 0 1px white);"
/>
</div>
{/if}
</div> </div>
{/snippet} {/snippet}

View File

@@ -4,6 +4,7 @@ interface DirectoryInfo {
id: number; id: number;
parentId: DirectoryId; parentId: DirectoryId;
name: string; name: string;
isFavorite?: boolean;
} }
interface FileInfo { interface FileInfo {
@@ -14,6 +15,7 @@ interface FileInfo {
createdAt?: Date; createdAt?: Date;
lastModifiedAt: Date; lastModifiedAt: Date;
categoryIds?: number[]; categoryIds?: number[];
isFavorite?: boolean;
} }
interface CategoryInfo { interface CategoryInfo {

View File

@@ -22,6 +22,7 @@ const cache = new FilesystemCache<CategoryId, MaybeCategoryInfo>({
name: fileInfo.name, name: fileInfo.name,
createdAt: fileInfo.createdAt, createdAt: fileInfo.createdAt,
lastModifiedAt: fileInfo.lastModifiedAt, lastModifiedAt: fileInfo.lastModifiedAt,
isFavorite: fileInfo.isFavorite,
isRecursive: file.isRecursive, isRecursive: file.isRecursive,
} }
: undefined; : undefined;
@@ -66,6 +67,7 @@ const cache = new FilesystemCache<CategoryId, MaybeCategoryInfo>({
parentId: file.parent, parentId: file.parent,
contentType: file.contentType, contentType: file.contentType,
isRecursive: file.isRecursive, isRecursive: file.isRecursive,
isFavorite: file.isFavorite,
...(await decryptFileMetadata(file, masterKey)), ...(await decryptFileMetadata(file, masterKey)),
})), })),
), ),

View File

@@ -27,6 +27,7 @@ const cache = new FilesystemCache<number, MaybeFileInfo>({
name: file.name, name: file.name,
createdAt: file.createdAt, createdAt: file.createdAt,
lastModifiedAt: file.lastModifiedAt, lastModifiedAt: file.lastModifiedAt,
isFavorite: file.isFavorite,
categories: categories?.filter((category) => !!category) ?? [], categories: categories?.filter((category) => !!category) ?? [],
}; };
} }
@@ -55,6 +56,7 @@ const cache = new FilesystemCache<number, MaybeFileInfo>({
name: metadata.name, name: metadata.name,
createdAt: metadata.createdAt, createdAt: metadata.createdAt,
lastModifiedAt: metadata.lastModifiedAt, lastModifiedAt: metadata.lastModifiedAt,
isFavorite: file.isFavorite,
categories, categories,
}); });
} catch (e) { } catch (e) {
@@ -121,6 +123,7 @@ const cache = new FilesystemCache<number, MaybeFileInfo>({
parentId: metadataRaw.parent, parentId: metadataRaw.parent,
contentType: metadataRaw.contentType, contentType: metadataRaw.contentType,
categories, categories,
isFavorite: metadataRaw.isFavorite,
...metadata, ...metadata,
}; };
}), }),

View File

@@ -641,6 +641,7 @@ export const searchFiles = async (
encName: file.encrypted_name, encName: file.encrypted_name,
encCreatedAt: file.encrypted_created_at, encCreatedAt: file.encrypted_created_at,
encLastModifiedAt: file.encrypted_last_modified_at, encLastModifiedAt: file.encrypted_last_modified_at,
isFavorite: file.is_favorite,
})); }));
}; };

View File

@@ -18,6 +18,7 @@
requestThumbnailUpload, requestThumbnailUpload,
requestFileAdditionToCategory, requestFileAdditionToCategory,
requestVideoStream, requestVideoStream,
requestFavoriteToggle,
} from "./service"; } from "./service";
import TopBarMenu from "./TopBarMenu.svelte"; import TopBarMenu from "./TopBarMenu.svelte";
@@ -75,6 +76,15 @@
void getFileInfo(data.id, $masterKeyStore?.get(1)?.key!); // TODO: FIXME 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(() => { $effect(() => {
HybridPromise.resolve(getFileInfo(data.id, $masterKeyStore?.get(1)?.key!)).then((result) => { HybridPromise.resolve(getFileInfo(data.id, $masterKeyStore?.get(1)?.key!)).then((result) => {
if (data.id === result.id) { if (data.id === result.id) {
@@ -158,6 +168,8 @@
{fileBlob} {fileBlob}
downloadUrl={videoStreamUrl} downloadUrl={videoStreamUrl}
filename={info?.name} filename={info?.name}
isFavorite={info?.isFavorite}
onToggleFavorite={toggleFavorite}
/> />
</div> </div>
</TopBar> </TopBar>

View File

@@ -5,6 +5,8 @@
import { fly } from "svelte/transition"; import { fly } from "svelte/transition";
import { goto } from "$app/navigation"; 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 IconFolderOpen from "~icons/material-symbols/folder-open";
import IconCloudDownload from "~icons/material-symbols/cloud-download"; import IconCloudDownload from "~icons/material-symbols/cloud-download";
@@ -13,10 +15,20 @@
downloadUrl?: string; downloadUrl?: string;
fileBlob?: Blob; fileBlob?: Blob;
filename?: string; filename?: string;
isFavorite?: boolean;
isOpen: 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 = () => { const handleDownload = () => {
if (fileBlob && filename) { if (fileBlob && filename) {
@@ -34,7 +46,7 @@
{#if isOpen && (directoryId || downloadUrl || fileBlob)} {#if isOpen && (directoryId || downloadUrl || fileBlob)}
<div <div
class="absolute right-2 top-full z-20 space-y-1 rounded-lg bg-white px-1 py-2 shadow-2xl" class="absolute right-2 top-full z-20 min-w-44 space-y-1 rounded-lg bg-white px-1 py-2 shadow-2xl"
transition:fly={{ y: -8, duration: 200 }} transition:fly={{ y: -8, duration: 200 }}
> >
<p class="px-3 pt-2 text-sm font-semibold text-gray-600">더보기</p> <p class="px-3 pt-2 text-sm font-semibold text-gray-600">더보기</p>
@@ -54,6 +66,13 @@
</button> </button>
{/snippet} {/snippet}
{#if typeof isFavorite === "boolean"}
{@render menuButton(
isFavorite ? IconFavorite : IconFavoriteOutline,
isFavorite ? "즐겨찾기 해제" : "즐겨찾기",
onToggleFavorite ?? (() => {}),
)}
{/if}
{#if directoryId} {#if directoryId}
{@render menuButton(IconFolderOpen, "폴더에서 보기", () => {@render menuButton(IconFolderOpen, "폴더에서 보기", () =>
goto( goto(

View File

@@ -48,3 +48,16 @@ export const requestFileAdditionToCategory = async (fileId: number, categoryId:
return false; 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;
}
};

View File

@@ -12,5 +12,5 @@
</script> </script>
<ActionEntryButton class="h-14" {onclick}> <ActionEntryButton class="h-14" {onclick}>
<DirectoryEntryLabel type="directory" name={info.name} /> <DirectoryEntryLabel type="directory" name={info.name} isFavorite={info.isFavorite} />
</ActionEntryButton> </ActionEntryButton>

View File

@@ -21,5 +21,6 @@
thumbnail={$thumbnail} thumbnail={$thumbnail}
name={info.name} name={info.name}
subtext={formatDateTime(info.createdAt ?? info.lastModifiedAt)} subtext={formatDateTime(info.createdAt ?? info.lastModifiedAt)}
isFavorite={info.isFavorite}
/> />
</ActionEntryButton> </ActionEntryButton>

View File

@@ -46,6 +46,7 @@ export const requestSearch = async (filter: SearchFilter, masterKey: CryptoKey)
exists: true, exists: true,
parentId: directory.parent, parentId: directory.parent,
...metadata, ...metadata,
isFavorite: !!directory.isFavorite,
}; };
}, },
}), }),
@@ -65,6 +66,7 @@ export const requestSearch = async (filter: SearchFilter, masterKey: CryptoKey)
exists: true, exists: true,
parentId: file.parent, parentId: file.parent,
contentType: file.contentType, contentType: file.contentType,
isFavorite: !!file.isFavorite,
...metadata, ...metadata,
}; };
}, },

View File

@@ -24,5 +24,10 @@
actionButtonIcon={onRemoveClick && IconClose} actionButtonIcon={onRemoveClick && IconClose}
onActionButtonClick={() => onRemoveClick?.(info)} onActionButtonClick={() => onRemoveClick?.(info)}
> >
<DirectoryEntryLabel type="file" thumbnail={$thumbnail} name={info.name} /> <DirectoryEntryLabel
type="file"
thumbnail={$thumbnail}
name={info.name}
isFavorite={info.isFavorite}
/>
</ActionEntryButton> </ActionEntryButton>

View File

@@ -40,5 +40,6 @@
thumbnail={$thumbnail} thumbnail={$thumbnail}
name={info.name} name={info.name}
subtext={formatDateTime(info.createdAt ?? info.lastModifiedAt)} subtext={formatDateTime(info.createdAt ?? info.lastModifiedAt)}
isFavorite={info.isFavorite}
/> />
</ActionEntryButton> </ActionEntryButton>

View File

@@ -31,5 +31,5 @@
actionButtonIcon={IconMoreVert} actionButtonIcon={IconMoreVert}
onActionButtonClick={() => action(onOpenMenuClick)} onActionButtonClick={() => action(onOpenMenuClick)}
> >
<DirectoryEntryLabel type="directory" name={info.name} /> <DirectoryEntryLabel type="directory" name={info.name} isFavorite={info.isFavorite} />
</ActionEntryButton> </ActionEntryButton>

View File

@@ -57,6 +57,7 @@ const categoryRouter = router({
createdAtIv: file.encCreatedAt?.iv, createdAtIv: file.encCreatedAt?.iv,
lastModifiedAt: file.encLastModifiedAt.ciphertext, lastModifiedAt: file.encLastModifiedAt.ciphertext,
lastModifiedAtIv: file.encLastModifiedAt.iv, lastModifiedAtIv: file.encLastModifiedAt.iv,
isFavorite: file.isFavorite,
isRecursive: file.isRecursive, isRecursive: file.isRecursive,
})), })),
}; };

View File

@@ -31,6 +31,7 @@ const fileRouter = router({
createdAtIv: file.encCreatedAt?.iv, createdAtIv: file.encCreatedAt?.iv,
lastModifiedAt: file.encLastModifiedAt.ciphertext, lastModifiedAt: file.encLastModifiedAt.ciphertext,
lastModifiedAtIv: file.encLastModifiedAt.iv, lastModifiedAtIv: file.encLastModifiedAt.iv,
isFavorite: file.isFavorite,
categories: categories.map((category) => ({ categories: categories.map((category) => ({
id: category.id, id: category.id,
parent: category.parentId, parent: category.parentId,
@@ -65,6 +66,7 @@ const fileRouter = router({
createdAtIv: file.encCreatedAt?.iv, createdAtIv: file.encCreatedAt?.iv,
lastModifiedAt: file.encLastModifiedAt.ciphertext, lastModifiedAt: file.encLastModifiedAt.ciphertext,
lastModifiedAtIv: file.encLastModifiedAt.iv, lastModifiedAtIv: file.encLastModifiedAt.iv,
isFavorite: file.isFavorite,
categories: file.categories.map((category) => ({ categories: file.categories.map((category) => ({
id: category.id, id: category.id,
parent: category.parentId, parent: category.parentId,

View File

@@ -32,6 +32,7 @@ const searchRouter = router({
dekVersion: directory.dekVersion, dekVersion: directory.dekVersion,
name: directory.encName.ciphertext, name: directory.encName.ciphertext,
nameIv: directory.encName.iv, nameIv: directory.encName.iv,
isFavorite: directory.isFavorite,
})), })),
files: files.map((file) => ({ files: files.map((file) => ({
id: file.id, id: file.id,
@@ -46,6 +47,7 @@ const searchRouter = router({
createdAtIv: file.encCreatedAt?.iv, createdAtIv: file.encCreatedAt?.iv,
lastModifiedAt: file.encLastModifiedAt.ciphertext, lastModifiedAt: file.encLastModifiedAt.ciphertext,
lastModifiedAtIv: file.encLastModifiedAt.iv, lastModifiedAtIv: file.encLastModifiedAt.iv,
isFavorite: file.isFavorite,
})), })),
}; };
}), }),