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";