summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--src/App.svelte48
-rw-r--r--src/lib/cache/autoCacheService.ts24
-rw-r--r--src/lib/cache/imageCache.ts76
-rw-r--r--src/lib/components/CacheDebug.svelte122
-rw-r--r--src/lib/components/CachedThumbnail.svelte5
-rw-r--r--src/lib/components/ImageList.svelte2
-rw-r--r--src/lib/components/ImagePreview.svelte4
7 files changed, 171 insertions, 110 deletions
diff --git a/src/App.svelte b/src/App.svelte
index cd63c9b..d224306 100644
--- a/src/App.svelte
+++ b/src/App.svelte
@@ -147,6 +147,7 @@
newImagePaths = updated;
}
+ let showDebug = $state(false);
let saving = $state(false);
async function saveToDevice() {
@@ -219,9 +220,7 @@
<div class="w-36 lg:w-40 shrink-0 border-l border-base-300 flex flex-col">
<div class="px-3 py-2 bg-base-100 border-b border-base-300 shrink-0 flex items-center overflow-hidden">
<span class="text-xs font-semibold whitespace-nowrap truncate">Photos {cachedCount}/{totalCount > 0 ? totalCount : images.length}</span>
- {#if isAutoCaching}
- <span class="loading loading-spinner loading-xs ml-1 shrink-0"></span>
- {/if}
+ <span class="ml-1.5 shrink-0 status status-secondary" class:status-ping={isAutoCaching}></span>
</div>
<div class="flex-1 min-h-0">
<ImageList {images} selectedPath={selectedFile?.path} onSelect={selectImage} {newImagePaths} onAnimationDone={handleAnimationDone} />
@@ -231,14 +230,22 @@
<!-- FAB Flower: bottom-right -->
<div class="fab fab-flower">
- <div tabindex="0" class="btn btn-lg btn-circle btn-primary">
+ <!-- Trigger: hamburger icon (shown when closed) -->
+ <div tabindex="0" class="btn btn-lg btn-circle btn-secondary">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-5">
<path stroke-linecap="round" stroke-linejoin="round" d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5" />
</svg>
</div>
- <div class="fab-close">
- <span class="btn btn-circle btn-lg btn-secondary">✕</span>
- </div>
+ <!-- Save/Download (center when opened) -->
+ <button class="btn btn-lg btn-circle btn-primary" onclick={() => void saveToDevice()} disabled={selectedFile === undefined || saving}>
+ {#if saving}
+ <span class="loading loading-spinner loading-sm"></span>
+ {:else}
+ <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-5">
+ <path stroke-linecap="round" stroke-linejoin="round" d="M3 16.5v2.25A2.25 2.25 0 0 0 5.25 21h13.5A2.25 2.25 0 0 0 21 18.75V16.5M16.5 12 12 16.5m0 0L7.5 12m4.5 4.5V3" />
+ </svg>
+ {/if}
+ </button>
<!-- Delete -->
<button class="btn btn-lg btn-circle btn-error" onclick={() => requestDelete()} disabled={selectedFile === undefined || deleting}>
{#if deleting}
@@ -255,15 +262,11 @@
<path stroke-linecap="round" stroke-linejoin="round" d="M20.25 7.5l-.625 10.632a2.25 2.25 0 0 1-2.247 2.118H6.622a2.25 2.25 0 0 1-2.247-2.118L3.75 7.5m6 4.125l2.25 2.25m0 0l2.25 2.25M12 13.875l2.25-2.25M12 13.875l-2.25 2.25M3.375 7.5h17.25c.621 0 1.125-.504 1.125-1.125v-1.5c0-.621-.504-1.125-1.125-1.125H3.375c-.621 0-1.125.504-1.125 1.125v1.5c0 .621.504 1.125 1.125 1.125Z" />
</svg>
</button>
- <!-- Save to device -->
- <button class="btn btn-lg btn-circle btn-secondary" onclick={() => void saveToDevice()} disabled={selectedFile === undefined || saving}>
- {#if saving}
- <span class="loading loading-spinner loading-sm"></span>
- {:else}
- <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-5">
- <path stroke-linecap="round" stroke-linejoin="round" d="M3 16.5v2.25A2.25 2.25 0 0 0 5.25 21h13.5A2.25 2.25 0 0 0 21 18.75V16.5M16.5 12 12 16.5m0 0L7.5 12m4.5 4.5V3" />
- </svg>
- {/if}
+ <!-- Debug toggle -->
+ <button class="btn btn-lg btn-circle btn-info" onclick={() => showDebug = !showDebug}>
+ <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-5">
+ <path stroke-linecap="round" stroke-linejoin="round" d="m11.25 11.25.041-.02a.75.75 0 0 1 1.063.852l-.708 2.836a.75.75 0 0 0 1.063.853l.041-.021M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Zm-9-3.75h.008v.008H12V8.25Z" />
+ </svg>
</button>
<!-- Dark mode toggle -->
<button class="btn btn-lg btn-circle btn-secondary" onclick={() => (isDark = !isDark)}>
@@ -277,15 +280,6 @@
</svg>
{/if}
</button>
- <!-- Refresh -->
- <button class="btn btn-lg btn-circle btn-secondary" onclick={() => loadAllImages()}>
- <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-5">
- <path stroke-linecap="round" stroke-linejoin="round" d="M4.5 12a7.5 7.5 0 0 1 12.57-5.55L19.5 8.87" />
- <path stroke-linecap="round" stroke-linejoin="round" d="M19.5 4.5v4.37h-4.37" />
- <path stroke-linecap="round" stroke-linejoin="round" d="M19.5 12a7.5 7.5 0 0 1-12.57 5.55L4.5 15.13" />
- <path stroke-linecap="round" stroke-linejoin="round" d="M4.5 19.5v-4.37h4.37" />
- </svg>
- </button>
</div>
<!-- Delete confirmation modal -->
@@ -305,4 +299,6 @@
</dialog>
{/if}
-<CacheDebug />
+{#if showDebug}
+ <CacheDebug />
+{/if}
diff --git a/src/lib/cache/autoCacheService.ts b/src/lib/cache/autoCacheService.ts
index 6d29598..b144f1d 100644
--- a/src/lib/cache/autoCacheService.ts
+++ b/src/lib/cache/autoCacheService.ts
@@ -45,6 +45,9 @@ class AutoCacheService {
/** The AbortController for the current background download. */
private _currentAbort: AbortController | undefined;
+ /** Current download speed in bytes/sec. 0 when idle. */
+ private _downloadSpeed = 0;
+
/** The image list to work through (newest first). */
private _images: FlashAirFileEntry[] = [];
@@ -100,6 +103,11 @@ class AutoCacheService {
return this._images.length;
}
+ /** Current download speed in bytes/second. 0 when not downloading. */
+ get downloadSpeed(): number {
+ return this._downloadSpeed;
+ }
+
/** Check if a path has been confirmed fully cached. */
isCached(path: string): boolean {
return this._cachedPaths.has(path);
@@ -245,7 +253,7 @@ class AutoCacheService {
if (reader === undefined) {
const blob = await res.blob();
if (abort.signal.aborted) return false;
- void imageCache.put('full', image.path, blob);
+ void imageCache.put('full', image.path, blob, undefined, image.date.getTime());
this._progressMap.set(image.path, 1);
this._notify();
return true;
@@ -253,6 +261,8 @@ class AutoCacheService {
const chunks: Uint8Array[] = [];
let received = 0;
+ let speedStartTime = performance.now();
+ let speedStartBytes = 0;
while (true) {
const { done, value } = await reader.read();
@@ -261,6 +271,17 @@ class AutoCacheService {
received += value.byteLength;
const progress = totalBytes > 0 ? received / totalBytes : 0;
this._progressMap.set(image.path, progress);
+
+ // Calculate speed every ~500ms to avoid excessive jitter
+ const now = performance.now();
+ const elapsed = now - speedStartTime;
+ if (elapsed >= 500) {
+ const bytesInWindow = received - speedStartBytes;
+ this._downloadSpeed = Math.round(bytesInWindow / (elapsed / 1000));
+ speedStartTime = now;
+ speedStartBytes = received;
+ }
+
this._notify();
}
@@ -278,6 +299,7 @@ class AutoCacheService {
return false;
} finally {
this._downloading = false;
+ this._downloadSpeed = 0;
this._currentAbort = undefined;
}
}
diff --git a/src/lib/cache/imageCache.ts b/src/lib/cache/imageCache.ts
index 5ff7393..0ccb975 100644
--- a/src/lib/cache/imageCache.ts
+++ b/src/lib/cache/imageCache.ts
@@ -15,6 +15,8 @@ export interface CachedImage {
readonly meta: ThumbnailMeta | undefined;
/** Unix timestamp (ms) when this entry was stored. */
readonly storedAt: number;
+ /** Unix timestamp (ms) of the file's date on the camera. Used for eviction priority. */
+ readonly fileDate: number | undefined;
}
type CacheKey = `${'thumbnail' | 'full'}:${string}`;
@@ -86,6 +88,7 @@ interface StoredRecord {
readonly blob: Blob;
readonly meta: ThumbnailMeta | undefined;
readonly storedAt: number;
+ readonly fileDate: number | undefined;
}
/** Read a single record from IndexedDB. Returns undefined on any failure. */
@@ -162,8 +165,44 @@ async function idbDelete(key: CacheKey): Promise<void> {
});
}
-/** 30 days in milliseconds. */
-const CACHE_TTL_MS = 30 * 24 * 60 * 60 * 1000;
+/** 3 days in milliseconds. */
+const CACHE_TTL_MS = 3 * 24 * 60 * 60 * 1000;
+
+/** Maximum total size of cached blobs (memory + IDB). 850 MB. */
+const MAX_CACHE_BYTES = 850 * 1024 * 1024;
+
+/** Current total bytes tracked in the memory cache. */
+let memoryCacheTotalBytes = 0;
+
+/**
+ * Evict the oldest entries from the cache until we're under the size budget.
+ * Evicts from both memory and IndexedDB.
+ */
+async function evictIfOverBudget(): Promise<void> {
+ if (memoryCacheTotalBytes <= MAX_CACHE_BYTES) return;
+
+ // Sort entries for eviction: oldest camera file date first.
+ // Full images before thumbnails (evict large items first for faster budget recovery).
+ // Entries without fileDate are evicted first (they're from older cache formats).
+ const entries = [...memoryCache.entries()].sort((a, b) => {
+ // Full images before thumbnails
+ if (a[1].kind !== b[1].kind) {
+ return a[1].kind === 'full' ? -1 : 1;
+ }
+ // Entries without fileDate go first (unknown = low priority)
+ const aDate = a[1].fileDate ?? 0;
+ const bDate = b[1].fileDate ?? 0;
+ // Oldest file date first (smallest timestamp = taken earliest = evict first)
+ return aDate - bDate;
+ });
+
+ for (const [key, entry] of entries) {
+ if (memoryCacheTotalBytes <= MAX_CACHE_BYTES) break;
+ memoryCacheTotalBytes -= entry.blob.size;
+ memoryCache.delete(key);
+ void idbDelete(key);
+ }
+}
function isExpired(storedAt: number): boolean {
return Date.now() - storedAt > CACHE_TTL_MS;
@@ -183,6 +222,7 @@ export const imageCache = {
const mem = memoryCache.get(key);
if (mem !== undefined) {
if (isExpired(mem.storedAt)) {
+ memoryCacheTotalBytes -= mem.blob.size;
memoryCache.delete(key);
void idbDelete(key);
return undefined;
@@ -204,9 +244,11 @@ export const imageCache = {
blob: record.blob,
meta: record.meta,
storedAt: record.storedAt,
+ fileDate: record.fileDate,
};
// Promote to memory for future fast lookups
memoryCache.set(key, cached);
+ memoryCacheTotalBytes += cached.blob.size;
return cached;
},
@@ -218,17 +260,31 @@ export const imageCache = {
* instantly visible to subsequent get() calls.
* IndexedDB persistence happens in the background.
*/
- async put(kind: 'thumbnail' | 'full', path: string, blob: Blob, meta?: ThumbnailMeta): Promise<void> {
+ async put(
+ kind: 'thumbnail' | 'full',
+ path: string,
+ blob: Blob,
+ meta?: ThumbnailMeta,
+ fileDate?: number,
+ ): Promise<void> {
const key = makeCacheKey(kind, path);
const storedAt = Date.now();
- const cached: CachedImage = { path, kind, blob, meta, storedAt };
+ const cached: CachedImage = { path, kind, blob, meta, storedAt, fileDate };
- // Instant: write to memory
+ // Instant: write to memory (replace existing if present)
+ const existing = memoryCache.get(key);
+ if (existing !== undefined) {
+ memoryCacheTotalBytes -= existing.blob.size;
+ }
memoryCache.set(key, cached);
+ memoryCacheTotalBytes += blob.size;
+
+ // Evict oldest entries if over budget
+ void evictIfOverBudget();
// Background: persist to IndexedDB
- const record: StoredRecord = { key, path, kind, blob, meta, storedAt };
+ const record: StoredRecord = { key, path, kind, blob, meta, storedAt, fileDate };
await idbPut(record);
},
@@ -237,6 +293,10 @@ export const imageCache = {
*/
async delete(kind: 'thumbnail' | 'full', path: string): Promise<void> {
const key = makeCacheKey(kind, path);
+ const existing = memoryCache.get(key);
+ if (existing !== undefined) {
+ memoryCacheTotalBytes -= existing.blob.size;
+ }
memoryCache.delete(key);
await idbDelete(key);
},
@@ -248,6 +308,7 @@ export const imageCache = {
// Prune in-memory
for (const [key, entry] of memoryCache) {
if (isExpired(entry.storedAt)) {
+ memoryCacheTotalBytes -= entry.blob.size;
memoryCache.delete(key);
}
}
@@ -294,6 +355,7 @@ export const imageCache = {
fullCount: number;
thumbCount: number;
totalBytes: number;
+ maxBytes: number;
idbEntries: number;
idbBytes: number;
idbError: string | undefined;
@@ -348,6 +410,7 @@ export const imageCache = {
fullCount,
thumbCount,
totalBytes,
+ maxBytes: MAX_CACHE_BYTES,
idbEntries,
idbBytes,
idbError,
@@ -361,6 +424,7 @@ export const imageCache = {
*/
async clear(): Promise<void> {
memoryCache.clear();
+ memoryCacheTotalBytes = 0;
const db = await openDb();
if (db === undefined) return;
diff --git a/src/lib/components/CacheDebug.svelte b/src/lib/components/CacheDebug.svelte
index 09e859d..ea1386d 100644
--- a/src/lib/components/CacheDebug.svelte
+++ b/src/lib/components/CacheDebug.svelte
@@ -1,11 +1,14 @@
<script lang="ts">
import { imageCache } from '../cache';
+ import { autoCacheService } from '../cache';
+ import { fly } from 'svelte/transition';
interface CacheStats {
entries: number;
fullCount: number;
thumbCount: number;
totalBytes: number;
+ maxBytes: number;
idbEntries: number;
idbBytes: number;
idbError: string | undefined;
@@ -14,8 +17,18 @@
}
let stats = $state<CacheStats | undefined>(undefined);
- let refreshing = $state(false);
- let collapsed = $state(false);
+
+ let idbBudgetPct = $derived(
+ stats !== undefined && stats.maxBytes > 0
+ ? (stats.idbBytes / stats.maxBytes * 100)
+ : 0
+ );
+
+ let budgetPct = $derived(
+ stats !== undefined && stats.maxBytes > 0
+ ? (stats.totalBytes / stats.maxBytes * 100)
+ : 0
+ );
function formatBytes(bytes: number): string {
if (bytes === 0) return '0 B';
@@ -26,45 +39,15 @@
return `${val.toFixed(1)} ${sizes[i]}`;
}
- let storageEstimate = $state<{ usage: number; quota: number } | undefined>(undefined);
- let storageError = $state<string | undefined>(undefined);
+ let downloadSpeed = $state(0);
async function refresh() {
- refreshing = true;
try {
stats = await imageCache.getStats();
- } catch { /* ignore */ }
-
- // Try standard Storage API first, then webkit fallback
- storageEstimate = undefined;
- storageError = undefined;
- try {
- if (navigator.storage && navigator.storage.estimate) {
- const est = await navigator.storage.estimate();
- storageEstimate = { usage: est.usage ?? 0, quota: est.quota ?? 0 };
- } else if ('webkitTemporaryStorage' in navigator) {
- // Chrome HTTP fallback
- const wk = (navigator as Record<string, unknown>)['webkitTemporaryStorage'] as
- { queryUsageAndQuota: (ok: (u: number, q: number) => void, err: (e: unknown) => void) => void } | undefined;
- if (wk !== undefined) {
- const result = await new Promise<{ usage: number; quota: number }>((resolve, reject) => {
- wk.queryUsageAndQuota(
- (usage, quota) => resolve({ usage, quota }),
- (err) => reject(err),
- );
- });
- storageEstimate = result;
- } else {
- storageError = 'Storage API not available (HTTP origin)';
- }
- } else {
- storageError = 'Storage API not available';
- }
- } catch (e) {
- storageError = e instanceof Error ? e.message : String(e);
+ } catch {
+ // ignore
}
-
- refreshing = false;
+ downloadSpeed = autoCacheService.downloadSpeed;
}
$effect(() => {
@@ -74,22 +57,33 @@
});
</script>
-<div class="fixed top-2 left-2 z-50 bg-base-100/95 backdrop-blur-sm rounded-box shadow-lg border border-base-300 p-3 text-xs font-mono max-w-80 pointer-events-auto">
- <button
- class="flex items-center justify-between w-full mb-1"
- onclick={() => collapsed = !collapsed}
- >
- <span class="font-bold text-sm">Cache Debug</span>
- <span class="text-base-content/50">{collapsed ? '▶' : '▼'}</span>
- </button>
+<div
+ class="fixed top-2 left-2 z-50 bg-base-100/95 backdrop-blur-sm rounded-box shadow-lg border border-base-300 p-3 text-xs font-mono max-w-80 pointer-events-auto"
+ transition:fly={{ x: -320, duration: 250 }}
+>
+ <div class="font-bold text-sm mb-2">Cache Debug</div>
- {#if !collapsed && stats !== undefined}
+ {#if stats !== undefined}
<div class="space-y-2">
+ <!-- Download Speed -->
+ <div>
+ <span class="font-semibold">DL Speed:</span>
+ {#if downloadSpeed > 0}
+ <span class="text-info">{formatBytes(downloadSpeed)}/s</span>
+ {:else}
+ <span class="text-base-content/50">N/A</span>
+ {/if}
+ </div>
+
<!-- Memory Cache -->
<div>
<div class="font-semibold text-primary">Memory Cache</div>
<div>Entries: <span class="text-info">{stats.entries}</span> ({stats.fullCount} full, {stats.thumbCount} thumb)</div>
- <div>Size: <span class="text-info">{formatBytes(stats.totalBytes)}</span></div>
+ <div>Size: <span class="text-info">{formatBytes(stats.totalBytes)}</span> / {formatBytes(stats.maxBytes)}</div>
+ <div>
+ <progress class="progress progress-primary w-full h-1.5" value={budgetPct} max="100"></progress>
+ <span class="text-[10px]" class:text-error={budgetPct > 90} class:text-warning={budgetPct > 70 && budgetPct <= 90}>{budgetPct.toFixed(1)}% of budget</span>
+ </div>
</div>
<!-- IndexedDB -->
@@ -99,46 +93,30 @@
<div class="text-error">Error: {stats.idbError}</div>
{:else}
<div>Entries: <span class="text-info">{stats.idbEntries}</span></div>
- <div>Size: <span class="text-info">{formatBytes(stats.idbBytes)}</span></div>
+ <div>Size: <span class="text-info">{formatBytes(stats.idbBytes)}</span> / {formatBytes(stats.maxBytes)}</div>
+ <div>
+ <progress class="progress progress-secondary w-full h-1.5" value={idbBudgetPct} max="100"></progress>
+ <span class="text-[10px]" class:text-error={idbBudgetPct > 90} class:text-warning={idbBudgetPct > 70 && idbBudgetPct <= 90}>{idbBudgetPct.toFixed(1)}% of budget</span>
+ </div>
{#if stats.idbWriteErrorCount > 0}
- <div class="text-error mt-1">❌ {stats.idbWriteErrorCount} write errors</div>
+ <div class="text-error mt-1">{stats.idbWriteErrorCount} write errors</div>
{#if stats.idbLastWriteError !== undefined}
<div class="text-error text-[10px] break-all">{stats.idbLastWriteError}</div>
{/if}
{/if}
<div class="mt-1">
{#if stats.idbEntries === stats.entries}
- <span class="text-success">✅ In sync with memory</span>
+ <span class="text-success">In sync with memory</span>
{:else if stats.idbEntries < stats.entries}
- <span class="text-warning">⏳ IDB behind ({stats.entries - stats.idbEntries} pending)</span>
+ <span class="text-warning">IDB behind ({stats.entries - stats.idbEntries} pending)</span>
{:else}
<span class="text-info">IDB has {stats.idbEntries - stats.entries} extra (pre-loaded)</span>
{/if}
</div>
{/if}
</div>
- <!-- Storage Quota -->
- <div class="border-t border-base-300 pt-2">
- <div class="font-semibold text-accent">Storage Quota</div>
- {#if storageEstimate !== undefined}
- <div>Used: <span class="text-info">{formatBytes(storageEstimate.usage)}</span> / {formatBytes(storageEstimate.quota)}</div>
- {#if storageEstimate.quota > 0}
- {@const pct = (storageEstimate.usage / storageEstimate.quota * 100)}
- <div>
- <progress class="progress progress-primary w-full h-2" value={pct} max="100"></progress>
- <span class:text-error={pct > 90} class:text-warning={pct > 70 && pct <= 90}>{pct.toFixed(1)}%</span>
- </div>
- {/if}
- {:else if storageError !== undefined}
- <div class="text-warning">{storageError}</div>
- {:else}
- <div class="text-base-content/50">Querying…</div>
- {/if}
- </div>
- </div>
- {:else if !collapsed}
- <div class="text-base-content/50">
- {#if refreshing}Loading…{:else}No data{/if}
</div>
+ {:else}
+ <div class="text-base-content/50">Loading…</div>
{/if}
</div>
diff --git a/src/lib/components/CachedThumbnail.svelte b/src/lib/components/CachedThumbnail.svelte
index 3a6d452..dada22a 100644
--- a/src/lib/components/CachedThumbnail.svelte
+++ b/src/lib/components/CachedThumbnail.svelte
@@ -6,9 +6,10 @@
interface Props {
path: string;
alt: string;
+ fileDate?: number;
}
- let { path, alt }: Props = $props();
+ let { path, alt, fileDate }: Props = $props();
let blobUrl = $state<string | undefined>(undefined);
let rawBlobUrl: string | undefined;
@@ -56,7 +57,7 @@
try {
const { blob, meta } = await flashair.fetchThumbnail(filePath);
// Store in cache (fire-and-forget)
- void imageCache.put('thumbnail', filePath, blob, meta);
+ void imageCache.put('thumbnail', filePath, blob, meta, fileDate);
const url = URL.createObjectURL(blob);
rawBlobUrl = url;
blobUrl = url;
diff --git a/src/lib/components/ImageList.svelte b/src/lib/components/ImageList.svelte
index 5048e1b..d56c95c 100644
--- a/src/lib/components/ImageList.svelte
+++ b/src/lib/components/ImageList.svelte
@@ -40,7 +40,7 @@
onclick={() => onSelect(file)}
>
{#if thumbUrl}
- <CachedThumbnail path={file.path} alt={file.filename} />
+ <CachedThumbnail path={file.path} alt={file.filename} fileDate={file.date.getTime()} />
{: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 914f9a1..335e231 100644
--- a/src/lib/components/ImagePreview.svelte
+++ b/src/lib/components/ImagePreview.svelte
@@ -320,7 +320,7 @@
// Check staleness after await
if (activePath !== entry.path) return;
// Store in cache (fire-and-forget)
- void imageCache.put('thumbnail', entry.path, blob, meta);
+ void imageCache.put('thumbnail', entry.path, blob, meta, entry.date.getTime());
const blobUrl = URL.createObjectURL(blob);
rawThumbnailUrl = blobUrl;
thumbnailBlobUrl = blobUrl;
@@ -386,7 +386,7 @@
const blob = await res.blob();
if (abort.signal.aborted || activePath !== entry.path) return;
// Store in cache (fire-and-forget)
- void imageCache.put('full', entry.path, blob);
+ void imageCache.put('full', entry.path, blob, undefined, entry.date.getTime());
autoCacheService.markCached(entry.path);
const objectUrl = URL.createObjectURL(blob);
rawObjectUrl = objectUrl;