썸네일 표시 구현

This commit is contained in:
static
2025-07-05 18:18:10 +09:00
parent eaf2d7f202
commit 9e67920968
4 changed files with 66 additions and 3 deletions

View File

@@ -10,18 +10,45 @@
name: string; name: string;
subtext?: string; subtext?: string;
textClass?: ClassValue; textClass?: ClassValue;
thumbnail?: ArrayBuffer;
type: "directory" | "file"; type: "directory" | "file";
} }
let { class: className, name, subtext, textClass: textClassName, type }: Props = $props(); let {
class: className,
name,
subtext,
textClass: textClassName,
thumbnail,
type,
}: Props = $props();
let thumbnailUrl: string | undefined = $state();
$effect(() => {
thumbnailUrl = thumbnail && URL.createObjectURL(new Blob([thumbnail]));
return () => thumbnailUrl && URL.revokeObjectURL(thumbnailUrl);
});
</script> </script>
{#snippet iconSnippet()}
<div class="flex h-10 w-10 items-center justify-center overflow-y-hidden text-xl">
{#if thumbnailUrl}
<img src={thumbnailUrl} alt={name} loading="lazy" />
{:else if type === "directory"}
<IconFolder />
{:else}
<IconDraft />
{/if}
</div>
{/snippet}
{#snippet subtextSnippet()} {#snippet subtextSnippet()}
{subtext} {subtext}
{/snippet} {/snippet}
<IconLabel <IconLabel
icon={type === "directory" ? IconFolder : IconDraft} icon={iconSnippet}
iconClass={type === "file" ? "text-blue-400" : undefined} iconClass={type === "file" ? "text-blue-400" : undefined}
subtext={subtext ? subtextSnippet : undefined} subtext={subtext ? subtextSnippet : undefined}
class={className} class={className}

View File

@@ -5,7 +5,7 @@
interface Props { interface Props {
children: Snippet; children: Snippet;
class?: ClassValue; class?: ClassValue;
icon: Component<SvelteHTMLElements["svg"]>; icon: Component<SvelteHTMLElements["svg"]> | Snippet;
iconClass?: ClassValue; iconClass?: ClassValue;
subtext?: Snippet; subtext?: Snippet;
textClass?: ClassValue; textClass?: ClassValue;

View File

@@ -4,6 +4,7 @@
import { DirectoryEntryLabel } from "$lib/components/molecules"; import { DirectoryEntryLabel } from "$lib/components/molecules";
import type { FileInfo } from "$lib/modules/filesystem"; import type { FileInfo } from "$lib/modules/filesystem";
import { formatDateTime } from "$lib/modules/util"; import { formatDateTime } from "$lib/modules/util";
import { getFileThumbnail } from "./service";
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";
@@ -16,6 +17,8 @@
let { info, onclick, onOpenMenuClick }: Props = $props(); let { info, onclick, onOpenMenuClick }: Props = $props();
let thumbnail: ArrayBuffer | undefined = $state();
const openFile = () => { const openFile = () => {
const { id, dataKey, dataKeyVersion, name } = $info!; const { id, dataKey, dataKeyVersion, name } = $info!;
if (!dataKey || !dataKeyVersion) return; // TODO: Error handling if (!dataKey || !dataKeyVersion) return; // TODO: Error handling
@@ -29,6 +32,21 @@
onOpenMenuClick({ type: "file", id, dataKey, dataKeyVersion, name }); onOpenMenuClick({ type: "file", id, dataKey, dataKeyVersion, name });
}; };
$effect(() => {
if ($info?.dataKey) {
getFileThumbnail($info.id, $info.dataKey)
.then((thumbnailData) => {
thumbnail = thumbnailData ?? undefined;
})
.catch(() => {
// TODO: Error handling
thumbnail = undefined;
});
} else {
thumbnail = undefined;
}
});
</script> </script>
{#if $info} {#if $info}
@@ -40,6 +58,7 @@
> >
<DirectoryEntryLabel <DirectoryEntryLabel
type="file" type="file"
{thumbnail}
name={$info.name} name={$info.name}
subtext={formatDateTime($info.createdAt ?? $info.lastModifiedAt)} subtext={formatDateTime($info.createdAt ?? $info.lastModifiedAt)}
/> />

View File

@@ -0,0 +1,17 @@
import { callGetApi } from "$lib/hooks";
import { decryptData } from "$lib/modules/crypto";
import type { FileThumbnailInfoResponse } from "$lib/server/schemas";
export const getFileThumbnail = async (fileId: number, dataKey: CryptoKey) => {
let res = await callGetApi(`/api/file/${fileId}/thumbnail`);
if (!res.ok) return null;
const { contentIv: thumbnailEncryptedIv }: FileThumbnailInfoResponse = await res.json();
res = await callGetApi(`/api/file/${fileId}/thumbnail/download`);
if (!res.ok) return null;
const thumbnailEncrypted = await res.arrayBuffer();
const thumbnail = await decryptData(thumbnailEncrypted, thumbnailEncryptedIv, dataKey);
return thumbnail;
};