summaryrefslogtreecommitdiffhomepage
path: root/src/lib/components
diff options
context:
space:
mode:
Diffstat (limited to 'src/lib/components')
-rw-r--r--src/lib/components/CachedThumbnail.svelte67
-rw-r--r--src/lib/components/ImageList.svelte8
-rw-r--r--src/lib/components/ImagePreview.svelte31
3 files changed, 100 insertions, 6 deletions
diff --git a/src/lib/components/CachedThumbnail.svelte b/src/lib/components/CachedThumbnail.svelte
new file mode 100644
index 0000000..f0fe8bf
--- /dev/null
+++ b/src/lib/components/CachedThumbnail.svelte
@@ -0,0 +1,67 @@
+<script lang="ts">
+ import { flashair } from '../flashair';
+ import { imageCache } from '../cache';
+
+ interface Props {
+ path: string;
+ alt: string;
+ }
+
+ let { path, alt }: Props = $props();
+
+ let blobUrl = $state<string | undefined>(undefined);
+ let rawBlobUrl: string | undefined;
+
+ $effect(() => {
+ const currentPath = path;
+ blobUrl = undefined;
+
+ void loadThumbnail(currentPath);
+
+ return () => {
+ if (rawBlobUrl !== undefined) {
+ URL.revokeObjectURL(rawBlobUrl);
+ rawBlobUrl = undefined;
+ }
+ };
+ });
+
+ async function loadThumbnail(filePath: string) {
+ // Try cache first
+ const cached = await imageCache.get('thumbnail', filePath);
+ if (cached !== undefined) {
+ const url = URL.createObjectURL(cached.blob);
+ rawBlobUrl = url;
+ blobUrl = url;
+ return;
+ }
+
+ // Fetch from card
+ const thumbUrl = flashair.thumbnailUrl(filePath);
+ if (thumbUrl === undefined) return;
+
+ try {
+ const { blob, meta } = await flashair.fetchThumbnail(filePath);
+ // Store in cache (fire-and-forget)
+ void imageCache.put('thumbnail', filePath, blob, meta);
+ const url = URL.createObjectURL(blob);
+ rawBlobUrl = url;
+ blobUrl = url;
+ } catch {
+ // Thumbnail fetch failed — show placeholder
+ }
+ }
+</script>
+
+{#if blobUrl !== undefined}
+ <img
+ src={blobUrl}
+ {alt}
+ class="w-full aspect-square rounded object-cover"
+ draggable="false"
+ />
+{:else}
+ <div class="w-full aspect-square rounded bg-base-300 flex items-center justify-center">
+ <span class="loading loading-spinner loading-sm"></span>
+ </div>
+{/if}
diff --git a/src/lib/components/ImageList.svelte b/src/lib/components/ImageList.svelte
index f338670..532cd3b 100644
--- a/src/lib/components/ImageList.svelte
+++ b/src/lib/components/ImageList.svelte
@@ -1,6 +1,7 @@
<script lang="ts">
import { flashair } from '../flashair';
import type { FlashAirFileEntry } from '../flashair';
+ import CachedThumbnail from './CachedThumbnail.svelte';
interface Props {
images: FlashAirFileEntry[];
@@ -29,12 +30,7 @@
onclick={() => onSelect(file)}
>
{#if thumbUrl}
- <img
- src={thumbUrl}
- alt={file.filename}
- class="w-full aspect-square rounded object-cover"
- loading="lazy"
- />
+ <CachedThumbnail path={file.path} alt={file.filename} />
{:else}
<div class="w-full aspect-square rounded bg-base-300 flex items-center justify-center">
<span class="text-lg">🖼️</span>
diff --git a/src/lib/components/ImagePreview.svelte b/src/lib/components/ImagePreview.svelte
index 08be8ac..4a59d13 100644
--- a/src/lib/components/ImagePreview.svelte
+++ b/src/lib/components/ImagePreview.svelte
@@ -1,6 +1,7 @@
<script lang="ts">
import { flashair } from '../flashair';
import type { FlashAirFileEntry } from '../flashair';
+ import { imageCache } from '../cache';
interface Props {
file: FlashAirFileEntry | undefined;
@@ -277,8 +278,23 @@
const url = flashair.thumbnailUrl(entry.path);
if (url === undefined) return;
+ // Try cache first
+ const cached = await imageCache.get('thumbnail', entry.path);
+ if (cached !== undefined) {
+ const blobUrl = URL.createObjectURL(cached.blob);
+ rawThumbnailUrl = blobUrl;
+ thumbnailBlobUrl = blobUrl;
+
+ if (cached.meta !== undefined && cached.meta.width !== undefined && cached.meta.height !== undefined && cached.meta.width > 0 && cached.meta.height > 0) {
+ imageAspectRatio = `${String(cached.meta.width)} / ${String(cached.meta.height)}`;
+ }
+ return;
+ }
+
try {
const { blob, meta } = await flashair.fetchThumbnail(entry.path);
+ // Store in cache (fire-and-forget)
+ void imageCache.put('thumbnail', entry.path, blob, meta);
const blobUrl = URL.createObjectURL(blob);
rawThumbnailUrl = blobUrl;
thumbnailBlobUrl = blobUrl;
@@ -310,6 +326,17 @@
progress = 0;
loadError = undefined;
+ // Try cache first
+ const cached = await imageCache.get('full', entry.path);
+ if (cached !== undefined && !abort.signal.aborted) {
+ const objectUrl = URL.createObjectURL(cached.blob);
+ rawObjectUrl = objectUrl;
+ fullObjectUrl = objectUrl;
+ progress = 1;
+ downloading = false;
+ return;
+ }
+
const url = flashair.fileUrl(entry.path);
const totalBytes = entry.size;
@@ -323,6 +350,8 @@
if (reader === undefined) {
const blob = await res.blob();
if (abort.signal.aborted) return;
+ // Store in cache (fire-and-forget)
+ void imageCache.put('full', entry.path, blob);
const objectUrl = URL.createObjectURL(blob);
rawObjectUrl = objectUrl;
fullObjectUrl = objectUrl;
@@ -345,6 +374,8 @@
if (abort.signal.aborted) return;
const blob = new Blob(chunks);
+ // Store in cache (fire-and-forget)
+ void imageCache.put('full', entry.path, blob);
const objectUrl = URL.createObjectURL(blob);
rawObjectUrl = objectUrl;
fullObjectUrl = objectUrl;