diff --git a/src/lib/components/atoms/buttons/FileThumbnailButton.svelte b/src/lib/components/atoms/buttons/FileThumbnailButton.svelte
index bb7761e..6c5632c 100644
--- a/src/lib/components/atoms/buttons/FileThumbnailButton.svelte
+++ b/src/lib/components/atoms/buttons/FileThumbnailButton.svelte
@@ -1,7 +1,6 @@
diff --git a/src/lib/components/organisms/Category/File.svelte b/src/lib/components/organisms/Category/File.svelte
index d4f5d4d..166de06 100644
--- a/src/lib/components/organisms/Category/File.svelte
+++ b/src/lib/components/organisms/Category/File.svelte
@@ -1,9 +1,8 @@
onRemoveClick?.(info)}
>
- {#await thumbnailPromise}
-
- {:then thumbnail}
-
- {/await}
+
diff --git a/src/lib/modules/file/cache.ts b/src/lib/modules/file/cache.ts
index ccb187e..fe3c66c 100644
--- a/src/lib/modules/file/cache.ts
+++ b/src/lib/modules/file/cache.ts
@@ -1,15 +1,12 @@
-import { LRUCache } from "lru-cache";
import {
getFileCacheIndex as getFileCacheIndexFromIndexedDB,
storeFileCacheIndex,
deleteFileCacheIndex,
type FileCacheIndex,
} from "$lib/indexedDB";
-import { readFile, writeFile, deleteFile, deleteDirectory } from "$lib/modules/opfs";
-import { getThumbnailUrl } from "$lib/modules/thumbnail";
+import { readFile, writeFile, deleteFile } from "$lib/modules/opfs";
const fileCacheIndex = new Map();
-const loadedThumbnails = new LRUCache({ max: 100 });
export const prepareFileCache = async () => {
for (const cache of await getFileCacheIndexFromIndexedDB()) {
@@ -51,30 +48,3 @@ export const deleteFileCache = async (fileId: number) => {
await deleteFile(`/cache/${fileId}`);
await deleteFileCacheIndex(fileId);
};
-
-export const getFileThumbnailCache = async (fileId: number) => {
- const thumbnail = loadedThumbnails.get(fileId);
- if (thumbnail) return thumbnail;
-
- const thumbnailBuffer = await readFile(`/thumbnail/file/${fileId}`);
- if (!thumbnailBuffer) return null;
-
- const thumbnailUrl = getThumbnailUrl(thumbnailBuffer);
- loadedThumbnails.set(fileId, thumbnailUrl);
- return thumbnailUrl;
-};
-
-export const storeFileThumbnailCache = async (fileId: number, thumbnailBuffer: ArrayBuffer) => {
- await writeFile(`/thumbnail/file/${fileId}`, thumbnailBuffer);
- loadedThumbnails.set(fileId, getThumbnailUrl(thumbnailBuffer));
-};
-
-export const deleteFileThumbnailCache = async (fileId: number) => {
- loadedThumbnails.delete(fileId);
- await deleteFile(`/thumbnail/file/${fileId}`);
-};
-
-export const deleteAllFileThumbnailCaches = async () => {
- loadedThumbnails.clear();
- await deleteDirectory("/thumbnail/file");
-};
diff --git a/src/lib/modules/file/index.ts b/src/lib/modules/file/index.ts
index 871d299..9e9ce0c 100644
--- a/src/lib/modules/file/index.ts
+++ b/src/lib/modules/file/index.ts
@@ -1,3 +1,4 @@
export * from "./cache";
export * from "./download.svelte";
+export * from "./thumbnail";
export * from "./upload.svelte";
diff --git a/src/lib/modules/file/thumbnail.ts b/src/lib/modules/file/thumbnail.ts
new file mode 100644
index 0000000..f923153
--- /dev/null
+++ b/src/lib/modules/file/thumbnail.ts
@@ -0,0 +1,90 @@
+import { LRUCache } from "lru-cache";
+import { writable, type Writable } from "svelte/store";
+import { browser } from "$app/environment";
+import { decryptData } from "$lib/modules/crypto";
+import type { SummarizedFileInfo } from "$lib/modules/filesystem";
+import { readFile, writeFile, deleteFile, deleteDirectory } from "$lib/modules/opfs";
+import { getThumbnailUrl } from "$lib/modules/thumbnail";
+import { isTRPCClientError, trpc } from "$trpc/client";
+
+const loadedThumbnails = new LRUCache>({ max: 100 });
+const loadingThumbnails = new Map>();
+
+const fetchFromOpfs = async (fileId: number) => {
+ const thumbnailBuffer = await readFile(`/thumbnail/file/${fileId}`);
+ if (thumbnailBuffer) {
+ return getThumbnailUrl(thumbnailBuffer);
+ }
+};
+
+const fetchFromServer = async (fileId: number, dataKey: CryptoKey) => {
+ try {
+ const [thumbnailEncrypted, { contentIv: thumbnailEncryptedIv }] = await Promise.all([
+ fetch(`/api/file/${fileId}/thumbnail/download`),
+ trpc().file.thumbnail.query({ id: fileId }),
+ ]);
+ const thumbnailBuffer = await decryptData(
+ await thumbnailEncrypted.arrayBuffer(),
+ thumbnailEncryptedIv,
+ dataKey,
+ );
+
+ void writeFile(`/thumbnail/file/${fileId}`, thumbnailBuffer);
+ return getThumbnailUrl(thumbnailBuffer);
+ } catch (e) {
+ if (isTRPCClientError(e) && e.data?.code === "NOT_FOUND") {
+ return null;
+ }
+ throw e;
+ }
+};
+
+export const getFileThumbnail = (file: SummarizedFileInfo) => {
+ if (
+ !browser ||
+ !(file.contentType.startsWith("image/") || file.contentType.startsWith("video/"))
+ ) {
+ return undefined;
+ }
+
+ const thumbnail = loadedThumbnails.get(file.id);
+ if (thumbnail) return thumbnail;
+
+ let loadingThumbnail = loadingThumbnails.get(file.id);
+ if (loadingThumbnail) return loadingThumbnail;
+
+ loadingThumbnail = writable(undefined);
+ loadingThumbnails.set(file.id, loadingThumbnail);
+
+ fetchFromOpfs(file.id)
+ .then((thumbnail) => thumbnail ?? (file.dataKey && fetchFromServer(file.id, file.dataKey.key)))
+ .then((thumbnail) => {
+ if (thumbnail) {
+ loadingThumbnail.set(thumbnail);
+ loadedThumbnails.set(file.id, loadingThumbnail as Writable);
+ }
+ loadingThumbnails.delete(file.id);
+ });
+ return loadingThumbnail;
+};
+
+export const storeFileThumbnailCache = async (fileId: number, thumbnailBuffer: ArrayBuffer) => {
+ await writeFile(`/thumbnail/file/${fileId}`, thumbnailBuffer);
+
+ const oldThumbnail = loadedThumbnails.get(fileId);
+ if (oldThumbnail) {
+ oldThumbnail.set(getThumbnailUrl(thumbnailBuffer));
+ } else {
+ loadedThumbnails.set(fileId, writable(getThumbnailUrl(thumbnailBuffer)));
+ }
+};
+
+export const deleteFileThumbnailCache = async (fileId: number) => {
+ loadedThumbnails.delete(fileId);
+ await deleteFile(`/thumbnail/file/${fileId}`);
+};
+
+export const deleteAllFileThumbnailCaches = async () => {
+ loadedThumbnails.clear();
+ await deleteDirectory(`/thumbnail/file`);
+};
diff --git a/src/lib/services/file.ts b/src/lib/services/file.ts
index da05824..05a92e1 100644
--- a/src/lib/services/file.ts
+++ b/src/lib/services/file.ts
@@ -1,15 +1,11 @@
import { getAllFileInfos } from "$lib/indexedDB/filesystem";
-import { decryptData } from "$lib/modules/crypto";
import {
getFileCache,
storeFileCache,
deleteFileCache,
- getFileThumbnailCache,
- storeFileThumbnailCache,
- deleteFileThumbnailCache,
downloadFile,
+ deleteFileThumbnailCache,
} from "$lib/modules/file";
-import { getThumbnailUrl } from "$lib/modules/thumbnail";
import type { FileThumbnailUploadRequest } from "$lib/server/schemas";
import { trpc } from "$trpc/client";
@@ -44,29 +40,6 @@ export const requestFileThumbnailUpload = async (
return await fetch(`/api/file/${fileId}/thumbnail/upload`, { method: "POST", body: form });
};
-export const requestFileThumbnailDownload = async (fileId: number, dataKey?: CryptoKey) => {
- const cache = await getFileThumbnailCache(fileId);
- if (cache || !dataKey) return cache;
-
- let thumbnailInfo;
- try {
- thumbnailInfo = await trpc().file.thumbnail.query({ id: fileId });
- } catch {
- // TODO: Error Handling
- return null;
- }
- const { contentIv: thumbnailEncryptedIv } = thumbnailInfo;
-
- const res = await fetch(`/api/file/${fileId}/thumbnail/download`);
- if (!res.ok) return null;
-
- const thumbnailEncrypted = await res.arrayBuffer();
- const thumbnailBuffer = await decryptData(thumbnailEncrypted, thumbnailEncryptedIv, dataKey);
-
- storeFileThumbnailCache(fileId, thumbnailBuffer); // Intended
- return getThumbnailUrl(thumbnailBuffer);
-};
-
export const requestDeletedFilesCleanup = async () => {
let liveFiles;
try {
diff --git a/src/routes/(main)/directory/[[id]]/DirectoryEntries/File.svelte b/src/routes/(main)/directory/[[id]]/DirectoryEntries/File.svelte
index 41f1a84..741bac3 100644
--- a/src/routes/(main)/directory/[[id]]/DirectoryEntries/File.svelte
+++ b/src/routes/(main)/directory/[[id]]/DirectoryEntries/File.svelte
@@ -1,9 +1,8 @@