heic 파일에 대한 썸네일 지원 추가 및 카테고리 페이지에서도 파일의 썸네일이 표시되도록 개선

This commit is contained in:
static
2025-07-06 19:55:13 +09:00
parent 8975a0200d
commit bcb969dc22
8 changed files with 66 additions and 29 deletions

View File

@@ -25,9 +25,9 @@
</script>
{#snippet iconSnippet()}
<div class="flex h-10 w-10 items-center justify-center overflow-y-hidden text-xl">
<div class="flex h-10 w-10 items-center justify-center text-xl">
{#if thumbnail}
<img src={thumbnail} alt={name} loading="lazy" />
<img src={thumbnail} alt={name} loading="lazy" class="aspect-square rounded object-cover" />
{:else if type === "directory"}
<IconFolder />
{:else}

View File

@@ -2,8 +2,9 @@
import type { Writable } from "svelte/store";
import { ActionEntryButton } from "$lib/components/atoms";
import { DirectoryEntryLabel } from "$lib/components/molecules";
import { getFileThumbnail } from "$lib/modules/file";
import type { FileInfo } from "$lib/modules/filesystem";
import type { SelectedFile } from "./service";
import { requestFileThumbnailDownload, type SelectedFile } from "./service";
import IconClose from "~icons/material-symbols/close";
@@ -15,6 +16,8 @@
let { info, onclick, onRemoveClick }: Props = $props();
let thumbnail: string | undefined = $state();
const openFile = () => {
const { id, dataKey, dataKeyVersion, name } = $info as FileInfo;
if (!dataKey || !dataKeyVersion) return; // TODO: Error handling
@@ -28,6 +31,24 @@
onRemoveClick!({ id, dataKey, dataKeyVersion, name });
};
$effect(() => {
if ($info?.dataKey) {
getFileThumbnail($info.id)
.then(
(thumbnailUrl) => thumbnailUrl || requestFileThumbnailDownload($info.id, $info.dataKey!),
)
.then((thumbnailUrl) => {
thumbnail = thumbnailUrl ?? undefined;
})
.catch(() => {
// TODO: Error Handling
thumbnail = undefined;
});
} else {
thumbnail = undefined;
}
});
</script>
{#if $info}
@@ -37,6 +58,6 @@
actionButtonIcon={onRemoveClick && IconClose}
onActionButtonClick={removeFile}
>
<DirectoryEntryLabel type="file" name={$info.name} />
<DirectoryEntryLabel type="file" {thumbnail} name={$info.name} />
</ActionEntryButton>
{/if}

View File

@@ -1,3 +1,5 @@
export { requestFileThumbnailDownload } from "$lib/services/file";
export interface SelectedFile {
id: number;
dataKey: CryptoKey;

View File

@@ -81,7 +81,11 @@ const extractExifDateTime = (fileBuffer: ArrayBuffer) => {
const generateThumbnail = async (file: File, fileType: string) => {
let url;
try {
if (fileType.startsWith("image/")) {
if (fileType === "image/heic") {
const { default: heic2any } = await import("heic2any");
url = URL.createObjectURL((await heic2any({ blob: file, toType: "image/png" })) as Blob);
return await generateImageThumbnail(url);
} else if (fileType.startsWith("image/")) {
url = URL.createObjectURL(file);
return await generateImageThumbnail(url);
} else if (fileType.startsWith("video/")) {

21
src/lib/services/file.ts Normal file
View File

@@ -0,0 +1,21 @@
import { callGetApi } from "$lib/hooks";
import { decryptData } from "$lib/modules/crypto";
import { storeFileThumbnail } from "$lib/modules/file";
import { getThumbnailUrl } from "$lib/modules/thumbnail";
import type { FileThumbnailInfoResponse } from "$lib/server/schemas";
export const requestFileThumbnailDownload = 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);
storeFileThumbnail(fileId, thumbnail); // Intended
return getThumbnailUrl(thumbnail);
};

View File

@@ -21,7 +21,16 @@ export const generateThumbnail = limitFunction(
async (fileBuffer: ArrayBuffer, fileType: string) => {
let url;
try {
if (fileType.startsWith("image/")) {
if (fileType === "image/heic") {
const { default: heic2any } = await import("heic2any");
url = URL.createObjectURL(
(await heic2any({
blob: new Blob([fileBuffer], { type: fileType }),
toType: "image/png",
})) as Blob,
);
return await generateImageThumbnail(url);
} else if (fileType.startsWith("image/")) {
url = URL.createObjectURL(new Blob([fileBuffer], { type: fileType }));
return await generateImageThumbnail(url);
} else if (fileType.startsWith("video/")) {

View File

@@ -13,8 +13,8 @@
</script>
{#if isFileUploading($status.status)}
<div class="flex h-14 items-center gap-x-4 p-2">
<div class="flex-shrink-0 text-lg">
<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">
<IconDraft class="text-gray-600" />
</div>
<div class="flex flex-grow flex-col overflow-hidden text-gray-800">

View File

@@ -1,21 +1 @@
import { callGetApi } from "$lib/hooks";
import { decryptData } from "$lib/modules/crypto";
import { storeFileThumbnail } from "$lib/modules/file";
import { getThumbnailUrl } from "$lib/modules/thumbnail";
import type { FileThumbnailInfoResponse } from "$lib/server/schemas";
export const requestFileThumbnailDownload = 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);
storeFileThumbnail(fileId, thumbnail); // Intended
return getThumbnailUrl(thumbnail);
};
export { requestFileThumbnailDownload } from "$lib/services/file";