정렬 관련 코드를 derived를 사용하도록 재작성

This commit is contained in:
static
2025-07-19 10:06:15 +09:00
parent 7f4ebd8601
commit 82c270ae99
12 changed files with 194 additions and 221 deletions

View File

@@ -1,10 +1,9 @@
<script lang="ts"> <script lang="ts">
import { untrack } from "svelte"; import { derived } from "svelte/store";
import { get, type Writable } from "svelte/store";
import { CheckBox } from "$lib/components/atoms"; import { CheckBox } from "$lib/components/atoms";
import { SubCategories, type SelectedCategory } from "$lib/components/molecules"; import { SubCategories, type SelectedCategory } from "$lib/components/molecules";
import type { CategoryInfo } from "$lib/modules/filesystem"; import type { CategoryInfo } from "$lib/modules/filesystem";
import { getFileInfo, type FileInfoStore } from "$lib/modules/filesystem2"; import { getFileInfo } from "$lib/modules/filesystem2";
import { SortBy, sortEntries } from "$lib/modules/util"; import { SortBy, sortEntries } from "$lib/modules/util";
import { masterKeyStore } from "$lib/stores"; import { masterKeyStore } from "$lib/stores";
import File from "./File.svelte"; import File from "./File.svelte";
@@ -34,37 +33,35 @@
isFileRecursive = $bindable(), isFileRecursive = $bindable(),
}: Props = $props(); }: Props = $props();
let files: { name?: string; info: FileInfoStore; isRecursive: boolean }[] = $state([]); let fileInfos = $derived(
info.files
$effect(() => { ?.filter(({ isRecursive }) => isFileRecursive || !isRecursive)
files = .map(({ id, isRecursive }) => ({
info.files info: getFileInfo(id, $masterKeyStore?.get(1)?.key!),
?.filter(({ isRecursive }) => isFileRecursive || !isRecursive) isRecursive,
.map(({ id, isRecursive }) => { })) ?? [],
const info = getFileInfo(id, $masterKeyStore?.get(1)?.key!); );
return { let files = $derived(
name: get(info).data?.name, derived(
info, fileInfos.map(({ info }) => info),
isRecursive, (infos) => {
}; const files = infos
}) ?? []; .map(($info, i) => {
if ($info.status === "success") {
const sort = () => { return {
sortEntries(files, sortBy); name: $info.data.name,
}; isRecursive: fileInfos[i]!.isRecursive,
return untrack(() => { info: $info.data,
sort(); };
}
const unsubscribes = files.map((file) => return undefined;
file.info.subscribe((value) => { })
if (file.name === value.data?.name) return; .filter((info) => info !== undefined);
file.name = value.data?.name; sortEntries(files, sortBy);
sort(); return files;
}), },
); ),
return () => unsubscribes.forEach((unsubscribe) => unsubscribe()); );
});
});
</script> </script>
<div class="space-y-4"> <div class="space-y-4">
@@ -90,7 +87,7 @@
</div> </div>
<div class="space-y-1"> <div class="space-y-1">
{#key info} {#key info}
{#each files as { info, isRecursive }} {#each $files as { info, isRecursive }}
<File <File
{info} {info}
onclick={onFileClick} onclick={onFileClick}

View File

@@ -1,14 +1,13 @@
<script lang="ts"> <script lang="ts">
import type { Writable } from "svelte/store";
import { ActionEntryButton } from "$lib/components/atoms"; import { ActionEntryButton } from "$lib/components/atoms";
import { DirectoryEntryLabel } from "$lib/components/molecules"; import { DirectoryEntryLabel } from "$lib/components/molecules";
import type { FileInfo, FileInfoStore } from "$lib/modules/filesystem2"; import type { FileInfo } from "$lib/modules/filesystem2";
import { requestFileThumbnailDownload, type SelectedFile } from "./service"; import { requestFileThumbnailDownload, type SelectedFile } from "./service";
import IconClose from "~icons/material-symbols/close"; import IconClose from "~icons/material-symbols/close";
interface Props { interface Props {
info: FileInfoStore; info: FileInfo;
onclick: (selectedFile: SelectedFile) => void; onclick: (selectedFile: SelectedFile) => void;
onRemoveClick?: (selectedFile: SelectedFile) => void; onRemoveClick?: (selectedFile: SelectedFile) => void;
} }
@@ -18,22 +17,22 @@
let thumbnail: string | undefined = $state(); let thumbnail: string | undefined = $state();
const openFile = () => { const openFile = () => {
const { id, dataKey, dataKeyVersion, name } = $info.data as FileInfo; const { id, dataKey, dataKeyVersion, name } = info;
if (!dataKey || !dataKeyVersion) return; // TODO: Error handling if (!dataKey || !dataKeyVersion) return; // TODO: Error handling
onclick({ id, dataKey, dataKeyVersion, name }); onclick({ id, dataKey, dataKeyVersion, name });
}; };
const removeFile = () => { const removeFile = () => {
const { id, dataKey, dataKeyVersion, name } = $info.data as FileInfo; const { id, dataKey, dataKeyVersion, name } = info;
if (!dataKey || !dataKeyVersion) return; // TODO: Error handling if (!dataKey || !dataKeyVersion) return; // TODO: Error handling
onRemoveClick!({ id, dataKey, dataKeyVersion, name }); onRemoveClick!({ id, dataKey, dataKeyVersion, name });
}; };
$effect(() => { $effect(() => {
if ($info.data?.dataKey) { if (info.dataKey) {
requestFileThumbnailDownload($info.data.id, $info.data.dataKey) requestFileThumbnailDownload(info.id, info.dataKey)
.then((thumbnailUrl) => { .then((thumbnailUrl) => {
thumbnail = thumbnailUrl ?? undefined; thumbnail = thumbnailUrl ?? undefined;
}) })
@@ -47,13 +46,11 @@
}); });
</script> </script>
{#if $info.status === "success"} <ActionEntryButton
<ActionEntryButton class="h-12"
class="h-12" onclick={openFile}
onclick={openFile} actionButtonIcon={onRemoveClick && IconClose}
actionButtonIcon={onRemoveClick && IconClose} onActionButtonClick={removeFile}
onActionButtonClick={removeFile} >
> <DirectoryEntryLabel type="file" {thumbnail} name={info.name} />
<DirectoryEntryLabel type="file" {thumbnail} name={$info.data.name} /> </ActionEntryButton>
</ActionEntryButton>
{/if}

View File

@@ -1,11 +1,24 @@
export const callGetApi = async (input: RequestInfo, fetchInternal = fetch) => { interface FetchOptions {
return await fetchInternal(input); fetch?: typeof fetch;
signal?: AbortSignal;
}
export const callGetApi = async (
input: RequestInfo,
{ fetch = globalThis.fetch, signal }: FetchOptions = {},
) => {
return await fetch(input, { method: "GET", signal });
}; };
export const callPostApi = async <T>(input: RequestInfo, payload?: T, fetchInternal = fetch) => { export const callPostApi = async <T>(
return await fetchInternal(input, { input: RequestInfo,
payload?: T,
{ fetch = globalThis.fetch, signal }: FetchOptions = {},
) => {
return await fetch(input, {
method: "POST", method: "POST",
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
body: payload ? JSON.stringify(payload) : undefined, body: payload ? JSON.stringify(payload) : undefined,
signal,
}); });
}; };

View File

@@ -1,5 +1,4 @@
import { useQueryClient, createQuery, createMutation } from "@tanstack/svelte-query"; import { useQueryClient, createQuery, createMutation } from "@tanstack/svelte-query";
import { browser } from "$app/environment";
import { callGetApi, callPostApi } from "$lib/hooks"; import { callGetApi, callPostApi } from "$lib/hooks";
import { import {
getDirectoryInfos as getDirectoryInfosFromIndexedDB, getDirectoryInfos as getDirectoryInfosFromIndexedDB,
@@ -44,17 +43,11 @@ export type DirectoryInfo =
subDirectoryIds: number[]; subDirectoryIds: number[];
fileIds: number[]; fileIds: number[];
}; };
export type SubDirectoryInfo = DirectoryInfo & { id: number };
const initializedIds = new Set<DirectoryId>();
let temporaryIdCounter = -1; let temporaryIdCounter = -1;
const getInitialDirectoryInfo = async (id: DirectoryId) => { const getInitialDirectoryInfo = async (id: DirectoryId) => {
if (!browser || initializedIds.has(id)) {
return undefined;
} else {
initializedIds.add(id);
}
const [directory, subDirectories, files] = await Promise.all([ const [directory, subDirectories, files] = await Promise.all([
id !== "root" ? getDirectoryInfoFromIndexedDB(id) : undefined, id !== "root" ? getDirectoryInfoFromIndexedDB(id) : undefined,
getDirectoryInfosFromIndexedDB(id), getDirectoryInfosFromIndexedDB(id),
@@ -72,16 +65,18 @@ const getInitialDirectoryInfo = async (id: DirectoryId) => {
}; };
export const getDirectoryInfo = (id: DirectoryId, masterKey: CryptoKey) => { export const getDirectoryInfo = (id: DirectoryId, masterKey: CryptoKey) => {
const queryClient = useQueryClient();
getInitialDirectoryInfo(id).then((info) => {
if (info && !queryClient.getQueryData(["directory", id])) {
queryClient.setQueryData<DirectoryInfo>(["directory", id], info);
}
}); // Intended
return createQuery<DirectoryInfo>({ return createQuery<DirectoryInfo>({
queryKey: ["directory", id], queryKey: ["directory", id],
queryFn: async () => { queryFn: async ({ client, signal }) => {
const res = await callGetApi(`/api/directory/${id}`); // TODO: 404 if (!client.getQueryData(["directory", id])) {
const initialInfo = await getInitialDirectoryInfo(id);
if (initialInfo) {
setTimeout(() => client.invalidateQueries({ queryKey: ["directory", id] }), 0);
return initialInfo;
}
}
const res = await callGetApi(`/api/directory/${id}`, { signal }); // TODO: 404
const { const {
metadata, metadata,
subDirectories: subDirectoryIds, subDirectories: subDirectoryIds,
@@ -202,7 +197,7 @@ export const useDirectoryRename = () => {
onMutate: async ({ id, newName }) => { onMutate: async ({ id, newName }) => {
await queryClient.cancelQueries({ queryKey: ["directory", id] }); await queryClient.cancelQueries({ queryKey: ["directory", id] });
const prevInfo = queryClient.getQueryData<DirectoryInfo & { id: number }>(["directory", id]); const prevInfo = queryClient.getQueryData<SubDirectoryInfo>(["directory", id]);
if (prevInfo) { if (prevInfo) {
queryClient.setQueryData<DirectoryInfo>(["directory", id], { queryClient.setQueryData<DirectoryInfo>(["directory", id], {
...prevInfo, ...prevInfo,
@@ -214,7 +209,7 @@ export const useDirectoryRename = () => {
}, },
onError: (_error, { id }, context) => { onError: (_error, { id }, context) => {
if (context?.oldName) { if (context?.oldName) {
queryClient.setQueryData<DirectoryInfo & { id: number }>(["directory", id], (prevInfo) => { queryClient.setQueryData<SubDirectoryInfo>(["directory", id], (prevInfo) => {
if (!prevInfo) return undefined; if (!prevInfo) return undefined;
return { ...prevInfo, name: context.oldName! }; return { ...prevInfo, name: context.oldName! };
}); });

View File

@@ -1,5 +1,4 @@
import { useQueryClient, createQuery, createMutation } from "@tanstack/svelte-query"; import { useQueryClient, createQuery, createMutation } from "@tanstack/svelte-query";
import { browser } from "$app/environment";
import { callGetApi, callPostApi } from "$lib/hooks"; import { callGetApi, callPostApi } from "$lib/hooks";
import { import {
getFileInfo as getFileInfoFromIndexedDB, getFileInfo as getFileInfoFromIndexedDB,
@@ -26,31 +25,23 @@ export interface FileInfo {
categoryIds: number[]; categoryIds: number[];
} }
const initializedFileIds = new Set<number>();
const getInitialFileInfo = async (id: number) => {
if (!browser || initializedFileIds.has(id)) {
return undefined;
}
initializedFileIds.add(id);
return await getFileInfoFromIndexedDB(id);
};
const decryptDate = async (ciphertext: string, iv: string, dataKey: CryptoKey) => { const decryptDate = async (ciphertext: string, iv: string, dataKey: CryptoKey) => {
return new Date(parseInt(await decryptString(ciphertext, iv, dataKey), 10)); return new Date(parseInt(await decryptString(ciphertext, iv, dataKey), 10));
}; };
export const getFileInfo = (id: number, masterKey: CryptoKey) => { export const getFileInfo = (id: number, masterKey: CryptoKey) => {
const queryClient = useQueryClient();
getInitialFileInfo(id).then((info) => {
if (info && !queryClient.getQueryData(["file", id])) {
queryClient.setQueryData<FileInfo>(["file", id], info);
}
}); // Intended
return createQuery<FileInfo>({ return createQuery<FileInfo>({
queryKey: ["file", id], queryKey: ["file", id],
queryFn: async () => { queryFn: async ({ client, signal }) => {
const res = await callGetApi(`/api/file/${id}`); // TODO: 404 if (!client.getQueryData(["file", id])) {
const initialInfo = await getFileInfoFromIndexedDB(id);
if (initialInfo) {
setTimeout(() => client.invalidateQueries({ queryKey: ["file", id] }), 0);
return initialInfo;
}
}
const res = await callGetApi(`/api/file/${id}`, { signal }); // TODO: 404
const metadata: FileInfoResponse = await res.json(); const metadata: FileInfoResponse = await res.json();
const { dataKey } = await unwrapDataKey(metadata.dek, masterKey); const { dataKey } = await unwrapDataKey(metadata.dek, masterKey);

View File

@@ -4,7 +4,7 @@ import type { MissingThumbnailFileScanResponse } from "$lib/server/schemas";
import type { PageLoad } from "./$types"; import type { PageLoad } from "./$types";
export const load: PageLoad = async ({ fetch }) => { export const load: PageLoad = async ({ fetch }) => {
const res = await callPostApi("/api/file/scanMissingThumbnails", undefined, fetch); const res = await callPostApi("/api/file/scanMissingThumbnails", undefined, { fetch });
if (!res.ok) { if (!res.ok) {
error(500, "Internal server error"); error(500, "Internal server error");
} }

View File

@@ -109,7 +109,7 @@
<UploadStatusCard onclick={() => goto("/file/uploads")} /> <UploadStatusCard onclick={() => goto("/file/uploads")} />
<DownloadStatusCard onclick={() => goto("/file/downloads")} /> <DownloadStatusCard onclick={() => goto("/file/downloads")} />
</div> </div>
{#key $info} {#key $info.data.id}
<DirectoryEntries <DirectoryEntries
info={$info.data} info={$info.data}
onEntryClick={({ type, id }) => goto(`/${type}/${id}`)} onEntryClick={({ type, id }) => goto(`/${type}/${id}`)}

View File

@@ -1,12 +1,11 @@
<script lang="ts"> <script lang="ts">
import { untrack } from "svelte"; import { derived } from "svelte/store";
import { get, type Writable } from "svelte/store";
import { import {
getDirectoryInfo, getDirectoryInfo,
getFileInfo, getFileInfo,
type DirectoryInfo, type DirectoryInfo,
type DirectoryInfoStore, type SubDirectoryInfo,
type FileInfoStore, type FileInfo,
} from "$lib/modules/filesystem2"; } from "$lib/modules/filesystem2";
import { SortBy, sortEntries } from "$lib/modules/util"; import { SortBy, sortEntries } from "$lib/modules/util";
import { import {
@@ -31,96 +30,84 @@
interface DirectoryEntry { interface DirectoryEntry {
name?: string; name?: string;
info: DirectoryInfoStore; info: SubDirectoryInfo;
} }
type FileEntry = type FileEntry =
| { | {
type: "file"; type: "file";
name?: string; name?: string;
info: FileInfoStore; info: FileInfo;
} }
| { | {
type: "uploading-file"; type: "uploading-file";
name: string; name: string;
info: Writable<FileUploadStatus>; info: FileUploadStatus;
}; };
let subDirectories: DirectoryEntry[] = $state([]); let subDirectories = $derived(
let files: FileEntry[] = $state([]); derived(
info.subDirectoryIds.map((id) => getDirectoryInfo(id, $masterKeyStore?.get(1)?.key!)),
$effect(() => { (infos) => {
// TODO: Fix duplicated requests const subDirectories = infos
.filter(($info) => $info.status === "success")
subDirectories = info.subDirectoryIds.map((id) => { .map(
const info = getDirectoryInfo(id, $masterKeyStore?.get(1)?.key!); ($info) =>
return { name: get(info).data?.name, info }; ({
}); name: $info.data.name,
files = info.fileIds info: $info.data as SubDirectoryInfo,
.map((id): FileEntry => { }) satisfies DirectoryEntry,
const info = getFileInfo(id, $masterKeyStore?.get(1)?.key!); );
return { sortEntries(subDirectories, sortBy);
type: "file", return subDirectories;
name: get(info).data?.name, },
info, ),
}; );
}) let files = $derived(
.concat( derived(
$fileUploadStatusStore info.fileIds.map((id) => getFileInfo(id, $masterKeyStore?.get(1)?.key!)),
.filter((statusStore) => { (infos) =>
const { parentId, status } = get(statusStore); infos
return parentId === info.id && isFileUploading(status); .filter(($info) => $info.status === "success")
}) .map(
.map((status) => ({ ($info) =>
type: "uploading-file", ({
name: get(status).name, type: "file",
info: status, name: $info.data.name,
})), info: $info.data,
); }) satisfies FileEntry,
),
const sort = () => { ),
sortEntries(subDirectories, sortBy); );
sortEntries(files, sortBy); let uploadingFiles = $derived(
}; derived($fileUploadStatusStore, (statuses) =>
return untrack(() => { statuses
sort(); .filter(({ parentId, status }) => parentId === info.id && isFileUploading(status))
.map(
const unsubscribes = subDirectories ($status) =>
.map((subDirectory) => ({
subDirectory.info.subscribe((value) => { type: "uploading-file",
if (subDirectory.name === value.data?.name) return; name: $status.name,
subDirectory.name = value.data?.name; info: $status,
sort(); }) satisfies FileEntry,
}), ),
) ),
.concat( );
files.map((file) => { let everyFiles = $derived(
if (file.type === "file") { derived([files, uploadingFiles], ([$files, $uploadingFiles]) => {
return file.info.subscribe((value) => { const allFiles = [...$files, ...$uploadingFiles];
if (file.name === value.data?.name) return; sortEntries(allFiles, sortBy);
file.name = value.data?.name; return allFiles;
sort(); }),
}); );
} else {
return file.info.subscribe((value) => {
if (file.name === value.name) return;
file.name = value.name;
sort();
});
}
}),
);
return () => unsubscribes.forEach((unsubscribe) => unsubscribe());
});
});
</script> </script>
{#if subDirectories.length + files.length > 0} {#if $subDirectories.length + $everyFiles.length > 0}
<div class="space-y-1 pb-[4.5rem]"> <div class="space-y-1 pb-[4.5rem]">
{#each subDirectories as { info }} {#each $subDirectories as { info }}
<SubDirectory {info} onclick={onEntryClick} onOpenMenuClick={onEntryMenuClick} /> <SubDirectory {info} onclick={onEntryClick} onOpenMenuClick={onEntryMenuClick} />
{/each} {/each}
{#each files as file} {#each $everyFiles as file}
{#if file.type === "file"} {#if file.type === "file"}
<File info={file.info} onclick={onEntryClick} onOpenMenuClick={onEntryMenuClick} /> <File info={file.info} onclick={onEntryClick} onOpenMenuClick={onEntryMenuClick} />
{:else} {:else}

View File

@@ -1,7 +1,7 @@
<script lang="ts"> <script lang="ts">
import { ActionEntryButton } from "$lib/components/atoms"; import { ActionEntryButton } from "$lib/components/atoms";
import { DirectoryEntryLabel } from "$lib/components/molecules"; import { DirectoryEntryLabel } from "$lib/components/molecules";
import type { FileInfo, FileInfoStore } from "$lib/modules/filesystem2"; import type { FileInfo } from "$lib/modules/filesystem2";
import { formatDateTime } from "$lib/modules/util"; import { formatDateTime } from "$lib/modules/util";
import { requestFileThumbnailDownload } from "./service"; import { requestFileThumbnailDownload } from "./service";
import type { SelectedEntry } from "../service.svelte"; import type { SelectedEntry } from "../service.svelte";
@@ -9,7 +9,7 @@
import IconMoreVert from "~icons/material-symbols/more-vert"; import IconMoreVert from "~icons/material-symbols/more-vert";
interface Props { interface Props {
info: FileInfoStore; info: FileInfo;
onclick: (selectedEntry: SelectedEntry) => void; onclick: (selectedEntry: SelectedEntry) => void;
onOpenMenuClick: (selectedEntry: SelectedEntry) => void; onOpenMenuClick: (selectedEntry: SelectedEntry) => void;
} }
@@ -19,22 +19,22 @@
let thumbnail: string | undefined = $state(); let thumbnail: string | undefined = $state();
const openFile = () => { const openFile = () => {
const { id, dataKey, dataKeyVersion, name } = $info.data as FileInfo; const { id, dataKey, dataKeyVersion, name } = info;
if (!dataKey || !dataKeyVersion) return; // TODO: Error handling if (!dataKey || !dataKeyVersion) return; // TODO: Error handling
onclick({ type: "file", id, dataKey, dataKeyVersion, name }); onclick({ type: "file", id, dataKey, dataKeyVersion, name });
}; };
const openMenu = () => { const openMenu = () => {
const { id, dataKey, dataKeyVersion, name } = $info.data as FileInfo; const { id, dataKey, dataKeyVersion, name } = info;
if (!dataKey || !dataKeyVersion) return; // TODO: Error handling if (!dataKey || !dataKeyVersion) return; // TODO: Error handling
onOpenMenuClick({ type: "file", id, dataKey, dataKeyVersion, name }); onOpenMenuClick({ type: "file", id, dataKey, dataKeyVersion, name });
}; };
$effect(() => { $effect(() => {
if ($info.data?.dataKey) { if (info.dataKey) {
requestFileThumbnailDownload($info.data.id, $info.data.dataKey) requestFileThumbnailDownload(info.id, info.dataKey)
.then((thumbnailUrl) => { .then((thumbnailUrl) => {
thumbnail = thumbnailUrl ?? undefined; thumbnail = thumbnailUrl ?? undefined;
}) })
@@ -48,18 +48,16 @@
}); });
</script> </script>
{#if $info.status === "success"} <ActionEntryButton
<ActionEntryButton class="h-14"
class="h-14" onclick={openFile}
onclick={openFile} actionButtonIcon={IconMoreVert}
actionButtonIcon={IconMoreVert} onActionButtonClick={openMenu}
onActionButtonClick={openMenu} >
> <DirectoryEntryLabel
<DirectoryEntryLabel type="file"
type="file" {thumbnail}
{thumbnail} name={info.name}
name={$info.data.name} subtext={formatDateTime(info.createdAt ?? info.lastModifiedAt)}
subtext={formatDateTime($info.data.createdAt ?? $info.data.lastModifiedAt)} />
/> </ActionEntryButton>
</ActionEntryButton>
{/if}

View File

@@ -1,15 +1,13 @@
<script lang="ts"> <script lang="ts">
import { ActionEntryButton } from "$lib/components/atoms"; import { ActionEntryButton } from "$lib/components/atoms";
import { DirectoryEntryLabel } from "$lib/components/molecules"; import { DirectoryEntryLabel } from "$lib/components/molecules";
import type { DirectoryInfo, DirectoryInfoStore } from "$lib/modules/filesystem2"; import type { SubDirectoryInfo } from "$lib/modules/filesystem2";
import type { SelectedEntry } from "../service.svelte"; import type { SelectedEntry } from "../service.svelte";
import IconMoreVert from "~icons/material-symbols/more-vert"; import IconMoreVert from "~icons/material-symbols/more-vert";
type SubDirectoryInfo = DirectoryInfo & { id: number };
interface Props { interface Props {
info: DirectoryInfoStore; info: SubDirectoryInfo;
onclick: (selectedEntry: SelectedEntry) => void; onclick: (selectedEntry: SelectedEntry) => void;
onOpenMenuClick: (selectedEntry: SelectedEntry) => void; onOpenMenuClick: (selectedEntry: SelectedEntry) => void;
} }
@@ -17,27 +15,25 @@
let { info, onclick, onOpenMenuClick }: Props = $props(); let { info, onclick, onOpenMenuClick }: Props = $props();
const openDirectory = () => { const openDirectory = () => {
const { id, dataKey, dataKeyVersion, name } = $info.data as SubDirectoryInfo; const { id, dataKey, dataKeyVersion, name } = info;
if (!dataKey || !dataKeyVersion) return; // TODO: Error handling if (!dataKey || !dataKeyVersion) return; // TODO: Error handling
onclick({ type: "directory", id, dataKey, dataKeyVersion, name }); onclick({ type: "directory", id, dataKey, dataKeyVersion, name });
}; };
const openMenu = () => { const openMenu = () => {
const { id, dataKey, dataKeyVersion, name } = $info.data as SubDirectoryInfo; const { id, dataKey, dataKeyVersion, name } = info;
if (!dataKey || !dataKeyVersion) return; // TODO: Error handling if (!dataKey || !dataKeyVersion) return; // TODO: Error handling
onOpenMenuClick({ type: "directory", id, dataKey, dataKeyVersion, name }); onOpenMenuClick({ type: "directory", id, dataKey, dataKeyVersion, name });
}; };
</script> </script>
{#if $info} <ActionEntryButton
<ActionEntryButton class="h-14"
class="h-14" onclick={openDirectory}
onclick={openDirectory} actionButtonIcon={IconMoreVert}
actionButtonIcon={IconMoreVert} onActionButtonClick={openMenu}
onActionButtonClick={openMenu} >
> <DirectoryEntryLabel type="directory" name={info.name} />
<DirectoryEntryLabel type="directory" name={$info.data?.name!} /> </ActionEntryButton>
</ActionEntryButton>
{/if}

View File

@@ -1,36 +1,35 @@
<script lang="ts"> <script lang="ts">
import type { Writable } from "svelte/store";
import { formatNetworkSpeed } from "$lib/modules/util"; import { formatNetworkSpeed } from "$lib/modules/util";
import { isFileUploading, type FileUploadStatus } from "$lib/stores"; import { isFileUploading, type FileUploadStatus } from "$lib/stores";
import IconDraft from "~icons/material-symbols/draft"; import IconDraft from "~icons/material-symbols/draft";
interface Props { interface Props {
status: Writable<FileUploadStatus>; status: FileUploadStatus;
} }
let { status }: Props = $props(); let { status }: Props = $props();
</script> </script>
{#if isFileUploading($status.status)} {#if isFileUploading(status.status)}
<div class="flex h-14 gap-x-4 p-2"> <div class="flex h-14 gap-x-4 p-2">
<div class="flex h-10 w-10 flex-shrink-0 items-center justify-center text-xl"> <div class="flex h-10 w-10 flex-shrink-0 items-center justify-center text-xl">
<IconDraft class="text-gray-600" /> <IconDraft class="text-gray-600" />
</div> </div>
<div class="flex flex-grow flex-col overflow-hidden text-gray-800"> <div class="flex flex-grow flex-col overflow-hidden text-gray-800">
<p title={$status.name} class="truncate font-medium"> <p title={status.name} class="truncate font-medium">
{$status.name} {status.name}
</p> </p>
<p class="text-xs"> <p class="text-xs">
{#if $status.status === "encryption-pending"} {#if status.status === "encryption-pending"}
준비 중 준비 중
{:else if $status.status === "encrypting"} {:else if status.status === "encrypting"}
암호화하는 중 암호화하는 중
{:else if $status.status === "upload-pending"} {:else if status.status === "upload-pending"}
업로드를 기다리는 중 업로드를 기다리는 중
{:else if $status.status === "uploading"} {:else if status.status === "uploading"}
전송됨 {Math.floor(($status.progress ?? 0) * 100)}% · 전송됨 {Math.floor((status.progress ?? 0) * 100)}% ·
{formatNetworkSpeed(($status.rate ?? 0) * 8)} {formatNetworkSpeed((status.rate ?? 0) * 8)}
{/if} {/if}
</p> </p>
</div> </div>

View File

@@ -4,7 +4,7 @@ import type { UserInfoResponse } from "$lib/server/schemas";
import type { PageLoad } from "./$types"; import type { PageLoad } from "./$types";
export const load: PageLoad = async ({ fetch }) => { export const load: PageLoad = async ({ fetch }) => {
const res = await callGetApi("/api/user", fetch); const res = await callGetApi("/api/user", { fetch });
if (!res.ok) { if (!res.ok) {
error(500, "Internal server error"); error(500, "Internal server error");
} }