mirror of
https://github.com/kmc7468/arkvault.git
synced 2026-02-03 23:56:53 +00:00
파일 페이지에서 즐겨찾기 설정이 가능하도록 변경 및 즐겨찾기에 추가된 경우 목록에서 즐겨찾기 여부를 아이콘으로 표시하도록 개선
This commit is contained in:
@@ -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 @@
|
||||
|
||||
<button
|
||||
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}
|
||||
<img src={$thumbnail} alt={info.name} class="h-full w-full object-cover" />
|
||||
{:else}
|
||||
<div class="h-full w-full bg-gray-100"></div>
|
||||
{/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>
|
||||
|
||||
@@ -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 @@
|
||||
</script>
|
||||
|
||||
{#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}
|
||||
<img src={thumbnail} alt={name} loading="lazy" class="aspect-square rounded object-cover" />
|
||||
{:else if type === "directory"}
|
||||
@@ -36,6 +39,14 @@
|
||||
{:else}
|
||||
<IconDraft class="text-blue-400" />
|
||||
{/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>
|
||||
{/snippet}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -22,6 +22,7 @@ const cache = new FilesystemCache<CategoryId, MaybeCategoryInfo>({
|
||||
name: fileInfo.name,
|
||||
createdAt: fileInfo.createdAt,
|
||||
lastModifiedAt: fileInfo.lastModifiedAt,
|
||||
isFavorite: fileInfo.isFavorite,
|
||||
isRecursive: file.isRecursive,
|
||||
}
|
||||
: undefined;
|
||||
@@ -66,6 +67,7 @@ const cache = new FilesystemCache<CategoryId, MaybeCategoryInfo>({
|
||||
parentId: file.parent,
|
||||
contentType: file.contentType,
|
||||
isRecursive: file.isRecursive,
|
||||
isFavorite: file.isFavorite,
|
||||
...(await decryptFileMetadata(file, masterKey)),
|
||||
})),
|
||||
),
|
||||
|
||||
@@ -27,6 +27,7 @@ const cache = new FilesystemCache<number, MaybeFileInfo>({
|
||||
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<number, MaybeFileInfo>({
|
||||
name: metadata.name,
|
||||
createdAt: metadata.createdAt,
|
||||
lastModifiedAt: metadata.lastModifiedAt,
|
||||
isFavorite: file.isFavorite,
|
||||
categories,
|
||||
});
|
||||
} catch (e) {
|
||||
@@ -121,6 +123,7 @@ const cache = new FilesystemCache<number, MaybeFileInfo>({
|
||||
parentId: metadataRaw.parent,
|
||||
contentType: metadataRaw.contentType,
|
||||
categories,
|
||||
isFavorite: metadataRaw.isFavorite,
|
||||
...metadata,
|
||||
};
|
||||
}),
|
||||
|
||||
@@ -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,
|
||||
}));
|
||||
};
|
||||
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
</div>
|
||||
</TopBar>
|
||||
|
||||
@@ -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)}
|
||||
<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 }}
|
||||
>
|
||||
<p class="px-3 pt-2 text-sm font-semibold text-gray-600">더보기</p>
|
||||
@@ -54,6 +66,13 @@
|
||||
</button>
|
||||
{/snippet}
|
||||
|
||||
{#if typeof isFavorite === "boolean"}
|
||||
{@render menuButton(
|
||||
isFavorite ? IconFavorite : IconFavoriteOutline,
|
||||
isFavorite ? "즐겨찾기 해제" : "즐겨찾기",
|
||||
onToggleFavorite ?? (() => {}),
|
||||
)}
|
||||
{/if}
|
||||
{#if directoryId}
|
||||
{@render menuButton(IconFolderOpen, "폴더에서 보기", () =>
|
||||
goto(
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -12,5 +12,5 @@
|
||||
</script>
|
||||
|
||||
<ActionEntryButton class="h-14" {onclick}>
|
||||
<DirectoryEntryLabel type="directory" name={info.name} />
|
||||
<DirectoryEntryLabel type="directory" name={info.name} isFavorite={info.isFavorite} />
|
||||
</ActionEntryButton>
|
||||
|
||||
@@ -21,5 +21,6 @@
|
||||
thumbnail={$thumbnail}
|
||||
name={info.name}
|
||||
subtext={formatDateTime(info.createdAt ?? info.lastModifiedAt)}
|
||||
isFavorite={info.isFavorite}
|
||||
/>
|
||||
</ActionEntryButton>
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
},
|
||||
|
||||
@@ -24,5 +24,10 @@
|
||||
actionButtonIcon={onRemoveClick && IconClose}
|
||||
onActionButtonClick={() => onRemoveClick?.(info)}
|
||||
>
|
||||
<DirectoryEntryLabel type="file" thumbnail={$thumbnail} name={info.name} />
|
||||
<DirectoryEntryLabel
|
||||
type="file"
|
||||
thumbnail={$thumbnail}
|
||||
name={info.name}
|
||||
isFavorite={info.isFavorite}
|
||||
/>
|
||||
</ActionEntryButton>
|
||||
|
||||
@@ -40,5 +40,6 @@
|
||||
thumbnail={$thumbnail}
|
||||
name={info.name}
|
||||
subtext={formatDateTime(info.createdAt ?? info.lastModifiedAt)}
|
||||
isFavorite={info.isFavorite}
|
||||
/>
|
||||
</ActionEntryButton>
|
||||
|
||||
@@ -31,5 +31,5 @@
|
||||
actionButtonIcon={IconMoreVert}
|
||||
onActionButtonClick={() => action(onOpenMenuClick)}
|
||||
>
|
||||
<DirectoryEntryLabel type="directory" name={info.name} />
|
||||
<DirectoryEntryLabel type="directory" name={info.name} isFavorite={info.isFavorite} />
|
||||
</ActionEntryButton>
|
||||
|
||||
@@ -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,
|
||||
})),
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
})),
|
||||
};
|
||||
}),
|
||||
|
||||
Reference in New Issue
Block a user