mirror of
https://github.com/kmc7468/arkvault.git
synced 2026-02-04 16:16:55 +00:00
즐겨찾기 기능 구현
This commit is contained in:
7
src/routes/(main)/favorites/+page.server.ts
Normal file
7
src/routes/(main)/favorites/+page.server.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { createCaller } from "$trpc/router.server";
|
||||
import type { PageServerLoad } from "./$types";
|
||||
|
||||
export const load: PageServerLoad = async (event) => {
|
||||
const favorites = await createCaller(event).favorites.get();
|
||||
return { favorites };
|
||||
};
|
||||
84
src/routes/(main)/favorites/+page.svelte
Normal file
84
src/routes/(main)/favorites/+page.svelte
Normal file
@@ -0,0 +1,84 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from "svelte";
|
||||
import { goto } from "$app/navigation";
|
||||
import { RowVirtualizer } from "$lib/components/atoms";
|
||||
import { masterKeyStore } from "$lib/stores";
|
||||
import Directory from "./Directory.svelte";
|
||||
import File from "./File.svelte";
|
||||
import { requestFavoriteEntries, requestRemoveFavorite, type FavoriteEntry } from "./service";
|
||||
|
||||
let { data } = $props();
|
||||
|
||||
let entries: FavoriteEntry[] = $state([]);
|
||||
let isLoading = $state(true);
|
||||
|
||||
onMount(async () => {
|
||||
const masterKey = $masterKeyStore?.get(1)?.key;
|
||||
if (masterKey) {
|
||||
entries = await requestFavoriteEntries(data.favorites, masterKey);
|
||||
}
|
||||
isLoading = false;
|
||||
});
|
||||
|
||||
const handleRemove = async (entry: FavoriteEntry) => {
|
||||
if (await requestRemoveFavorite(entry.type, entry.details.id)) {
|
||||
entries = entries.filter(
|
||||
(e) => !(e.type === entry.type && e.details.id === entry.details.id),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const handleClick = (entry: FavoriteEntry) => {
|
||||
goto(
|
||||
entry.type === "file"
|
||||
? `/file/${entry.details.id}?from=favorite`
|
||||
: `/directory/${entry.details.id}?from=favorite`,
|
||||
);
|
||||
};
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>즐겨찾기</title>
|
||||
</svelte:head>
|
||||
|
||||
<div class="flex h-full flex-col p-4">
|
||||
{#if isLoading}
|
||||
<div class="flex flex-grow items-center justify-center">
|
||||
<p class="text-gray-500">
|
||||
{#if data.favorites.files.length === 0 && data.favorites.directories.length === 0}
|
||||
즐겨찾기한 항목이 없어요.
|
||||
{:else}
|
||||
로딩 중...
|
||||
{/if}
|
||||
</p>
|
||||
</div>
|
||||
{:else if entries.length === 0}
|
||||
<div class="flex flex-grow items-center justify-center">
|
||||
<p class="text-gray-500">즐겨찾기한 항목이 없어요.</p>
|
||||
</div>
|
||||
{:else}
|
||||
<RowVirtualizer
|
||||
count={entries.length}
|
||||
getItemKey={(index) => `${entries[index]!.type}-${entries[index]!.details.id}`}
|
||||
estimateItemHeight={() => 56}
|
||||
itemGap={4}
|
||||
>
|
||||
{#snippet item(index)}
|
||||
{@const entry = entries[index]!}
|
||||
{#if entry.type === "directory"}
|
||||
<Directory
|
||||
info={entry.details}
|
||||
onclick={() => handleClick(entry)}
|
||||
onRemoveClick={() => handleRemove(entry)}
|
||||
/>
|
||||
{:else}
|
||||
<File
|
||||
info={entry.details}
|
||||
onclick={() => handleClick(entry)}
|
||||
onRemoveClick={() => handleRemove(entry)}
|
||||
/>
|
||||
{/if}
|
||||
{/snippet}
|
||||
</RowVirtualizer>
|
||||
{/if}
|
||||
</div>
|
||||
24
src/routes/(main)/favorites/Directory.svelte
Normal file
24
src/routes/(main)/favorites/Directory.svelte
Normal file
@@ -0,0 +1,24 @@
|
||||
<script lang="ts">
|
||||
import { ActionEntryButton } from "$lib/components/atoms";
|
||||
import { DirectoryEntryLabel } from "$lib/components/molecules";
|
||||
import type { SubDirectoryInfo } from "$lib/modules/filesystem";
|
||||
|
||||
import IconClose from "~icons/material-symbols/close";
|
||||
|
||||
interface Props {
|
||||
info: SubDirectoryInfo;
|
||||
onclick: () => void;
|
||||
onRemoveClick: () => void;
|
||||
}
|
||||
|
||||
let { info, onclick, onRemoveClick }: Props = $props();
|
||||
</script>
|
||||
|
||||
<ActionEntryButton
|
||||
class="h-14"
|
||||
{onclick}
|
||||
actionButtonIcon={IconClose}
|
||||
onActionButtonClick={onRemoveClick}
|
||||
>
|
||||
<DirectoryEntryLabel type="directory" name={info.name} />
|
||||
</ActionEntryButton>
|
||||
27
src/routes/(main)/favorites/File.svelte
Normal file
27
src/routes/(main)/favorites/File.svelte
Normal file
@@ -0,0 +1,27 @@
|
||||
<script lang="ts">
|
||||
import { ActionEntryButton } from "$lib/components/atoms";
|
||||
import { DirectoryEntryLabel } from "$lib/components/molecules";
|
||||
import { getFileThumbnail } from "$lib/modules/file";
|
||||
import type { SummarizedFileInfo } from "$lib/modules/filesystem";
|
||||
|
||||
import IconClose from "~icons/material-symbols/close";
|
||||
|
||||
interface Props {
|
||||
info: SummarizedFileInfo;
|
||||
onclick: () => void;
|
||||
onRemoveClick: () => void;
|
||||
}
|
||||
|
||||
let { info, onclick, onRemoveClick }: Props = $props();
|
||||
|
||||
let thumbnail = $derived(getFileThumbnail(info));
|
||||
</script>
|
||||
|
||||
<ActionEntryButton
|
||||
class="h-14"
|
||||
{onclick}
|
||||
actionButtonIcon={IconClose}
|
||||
onActionButtonClick={onRemoveClick}
|
||||
>
|
||||
<DirectoryEntryLabel type="file" thumbnail={$thumbnail} name={info.name} />
|
||||
</ActionEntryButton>
|
||||
86
src/routes/(main)/favorites/service.ts
Normal file
86
src/routes/(main)/favorites/service.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
import {
|
||||
decryptDirectoryMetadata,
|
||||
decryptFileMetadata,
|
||||
getFileInfo,
|
||||
type SummarizedFileInfo,
|
||||
type SubDirectoryInfo,
|
||||
} from "$lib/modules/filesystem";
|
||||
import { HybridPromise, sortEntries } from "$lib/utils";
|
||||
import { trpc } from "$trpc/client";
|
||||
import type { RouterOutputs } from "$trpc/router.server";
|
||||
|
||||
export type FavoriteEntry =
|
||||
| { type: "directory"; name: string; details: SubDirectoryInfo }
|
||||
| { type: "file"; name: string; details: SummarizedFileInfo };
|
||||
|
||||
export const requestFavoriteEntries = async (
|
||||
favorites: RouterOutputs["favorites"]["get"],
|
||||
masterKey: CryptoKey,
|
||||
): Promise<FavoriteEntry[]> => {
|
||||
const directories: FavoriteEntry[] = await Promise.all(
|
||||
favorites.directories.map(async (dir) => {
|
||||
const metadata = await decryptDirectoryMetadata(dir, masterKey);
|
||||
return {
|
||||
type: "directory" as const,
|
||||
name: metadata.name,
|
||||
details: {
|
||||
id: dir.id,
|
||||
parentId: dir.parent,
|
||||
isFavorite: true,
|
||||
dataKey: metadata.dataKey,
|
||||
name: metadata.name,
|
||||
} as SubDirectoryInfo,
|
||||
};
|
||||
}),
|
||||
);
|
||||
|
||||
const fileResults = await Promise.all(
|
||||
favorites.files.map(async (file) => {
|
||||
const result = await HybridPromise.resolve(
|
||||
getFileInfo(file.id, masterKey, {
|
||||
async fetchFromServer(id, cachedInfo) {
|
||||
const metadata = await decryptFileMetadata(file, masterKey);
|
||||
return {
|
||||
categories: [],
|
||||
...cachedInfo,
|
||||
id: id as number,
|
||||
exists: true,
|
||||
parentId: file.parent,
|
||||
contentType: file.contentType,
|
||||
isFavorite: true,
|
||||
...metadata,
|
||||
};
|
||||
},
|
||||
}),
|
||||
);
|
||||
if (result?.exists) {
|
||||
return {
|
||||
type: "file" as const,
|
||||
name: result.name,
|
||||
details: result as SummarizedFileInfo,
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}),
|
||||
);
|
||||
|
||||
const files = fileResults.filter(
|
||||
(f): f is { type: "file"; name: string; details: SummarizedFileInfo } => f !== null,
|
||||
);
|
||||
|
||||
return [...sortEntries(directories), ...sortEntries(files)];
|
||||
};
|
||||
|
||||
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;
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user