From bcb969dc22389267c93d54acf42560e7855040a5 Mon Sep 17 00:00:00 2001 From: static Date: Sun, 6 Jul 2025 19:55:13 +0900 Subject: [PATCH] =?UTF-8?q?heic=20=ED=8C=8C=EC=9D=BC=EC=97=90=20=EB=8C=80?= =?UTF-8?q?=ED=95=9C=20=EC=8D=B8=EB=84=A4=EC=9D=BC=20=EC=A7=80=EC=9B=90=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20=EB=B0=8F=20=EC=B9=B4=ED=85=8C=EA=B3=A0?= =?UTF-8?q?=EB=A6=AC=20=ED=8E=98=EC=9D=B4=EC=A7=80=EC=97=90=EC=84=9C?= =?UTF-8?q?=EB=8F=84=20=ED=8C=8C=EC=9D=BC=EC=9D=98=20=EC=8D=B8=EB=84=A4?= =?UTF-8?q?=EC=9D=BC=EC=9D=B4=20=ED=91=9C=EC=8B=9C=EB=90=98=EB=8F=84?= =?UTF-8?q?=EB=A1=9D=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../labels/DirectoryEntryLabel.svelte | 4 +-- .../components/organisms/Category/File.svelte | 25 +++++++++++++++++-- .../components/organisms/Category/service.ts | 2 ++ src/lib/modules/file/upload.ts | 6 ++++- src/lib/services/file.ts | 21 ++++++++++++++++ .../settings/thumbnails/service.ts | 11 +++++++- .../DirectoryEntries/UploadingFile.svelte | 4 +-- .../[[id]]/DirectoryEntries/service.ts | 22 +--------------- 8 files changed, 66 insertions(+), 29 deletions(-) create mode 100644 src/lib/services/file.ts diff --git a/src/lib/components/molecules/labels/DirectoryEntryLabel.svelte b/src/lib/components/molecules/labels/DirectoryEntryLabel.svelte index e38b348..57523b5 100644 --- a/src/lib/components/molecules/labels/DirectoryEntryLabel.svelte +++ b/src/lib/components/molecules/labels/DirectoryEntryLabel.svelte @@ -25,9 +25,9 @@ {#snippet iconSnippet()} -
+
{#if thumbnail} - {name} + {name} {:else if type === "directory"} {:else} diff --git a/src/lib/components/organisms/Category/File.svelte b/src/lib/components/organisms/Category/File.svelte index 5263b95..23465c1 100644 --- a/src/lib/components/organisms/Category/File.svelte +++ b/src/lib/components/organisms/Category/File.svelte @@ -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; + } + }); {#if $info} @@ -37,6 +58,6 @@ actionButtonIcon={onRemoveClick && IconClose} onActionButtonClick={removeFile} > - + {/if} diff --git a/src/lib/components/organisms/Category/service.ts b/src/lib/components/organisms/Category/service.ts index 1d587b5..fb6e640 100644 --- a/src/lib/components/organisms/Category/service.ts +++ b/src/lib/components/organisms/Category/service.ts @@ -1,3 +1,5 @@ +export { requestFileThumbnailDownload } from "$lib/services/file"; + export interface SelectedFile { id: number; dataKey: CryptoKey; diff --git a/src/lib/modules/file/upload.ts b/src/lib/modules/file/upload.ts index b56375f..3f70b13 100644 --- a/src/lib/modules/file/upload.ts +++ b/src/lib/modules/file/upload.ts @@ -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/")) { diff --git a/src/lib/services/file.ts b/src/lib/services/file.ts new file mode 100644 index 0000000..70d8887 --- /dev/null +++ b/src/lib/services/file.ts @@ -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); +}; diff --git a/src/routes/(fullscreen)/settings/thumbnails/service.ts b/src/routes/(fullscreen)/settings/thumbnails/service.ts index ad24954..e3c828c 100644 --- a/src/routes/(fullscreen)/settings/thumbnails/service.ts +++ b/src/routes/(fullscreen)/settings/thumbnails/service.ts @@ -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/")) { diff --git a/src/routes/(main)/directory/[[id]]/DirectoryEntries/UploadingFile.svelte b/src/routes/(main)/directory/[[id]]/DirectoryEntries/UploadingFile.svelte index 7977c53..a6df05a 100644 --- a/src/routes/(main)/directory/[[id]]/DirectoryEntries/UploadingFile.svelte +++ b/src/routes/(main)/directory/[[id]]/DirectoryEntries/UploadingFile.svelte @@ -13,8 +13,8 @@ {#if isFileUploading($status.status)} -
-
+
+
diff --git a/src/routes/(main)/directory/[[id]]/DirectoryEntries/service.ts b/src/routes/(main)/directory/[[id]]/DirectoryEntries/service.ts index 70d8887..d4b47f8 100644 --- a/src/routes/(main)/directory/[[id]]/DirectoryEntries/service.ts +++ b/src/routes/(main)/directory/[[id]]/DirectoryEntries/service.ts @@ -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";