diff options
| author | Adam Malczewski <[email protected]> | 2026-04-09 17:36:24 +0900 |
|---|---|---|
| committer | Adam Malczewski <[email protected]> | 2026-04-09 17:36:24 +0900 |
| commit | 76acef93e851641bb574f89ef62729bb38354c20 (patch) | |
| tree | 76b036fb36844ab1422d48dc5dc31ce790329c47 /src/lib | |
| parent | db1b2315b78d2b358fa8b375ae0216c408ed097e (diff) | |
| download | flashair-speedsync-76acef93e851641bb574f89ef62729bb38354c20.tar.gz flashair-speedsync-76acef93e851641bb574f89ef62729bb38354c20.zip | |
loading image on open
Diffstat (limited to 'src/lib')
| -rw-r--r-- | src/lib/components/ImagePreview.svelte | 151 |
1 files changed, 137 insertions, 14 deletions
diff --git a/src/lib/components/ImagePreview.svelte b/src/lib/components/ImagePreview.svelte index 82115e7..d811c4f 100644 --- a/src/lib/components/ImagePreview.svelte +++ b/src/lib/components/ImagePreview.svelte @@ -8,33 +8,156 @@ let { file }: Props = $props(); - let imageUrl = $derived(file !== undefined ? flashair.fileUrl(file.path) : undefined); - let imageLoaded = $state(false); + let thumbnailUrl = $derived( + file !== undefined ? flashair.thumbnailUrl(file.path) : undefined, + ); + + let fullObjectUrl = $state<string | undefined>(undefined); + let progress = $state(0); + let downloading = $state(false); + let loadError = $state<string | undefined>(undefined); + + let currentAbort: AbortController | undefined; + + /** + * Plain (non-reactive) mirror of fullObjectUrl so we can revoke it + * without reading the $state variable inside $effect (which would + * add it as a tracked dependency and cause an infinite loop). + */ + let rawObjectUrl: string | undefined; $effect(() => { - if (imageUrl !== undefined) { - imageLoaded = false; + const currentFile = file; + if (currentFile === undefined) { + cleanup(); + return; } + + loadFullImage(currentFile); + + return () => { + if (currentAbort !== undefined) { + currentAbort.abort(); + currentAbort = undefined; + } + }; }); + + function cleanup() { + if (rawObjectUrl !== undefined) { + URL.revokeObjectURL(rawObjectUrl); + rawObjectUrl = undefined; + } + fullObjectUrl = undefined; + progress = 0; + downloading = false; + loadError = undefined; + } + + async function loadFullImage(entry: FlashAirFileEntry) { + cleanup(); + + if (currentAbort !== undefined) { + currentAbort.abort(); + } + const abort = new AbortController(); + currentAbort = abort; + + downloading = true; + progress = 0; + loadError = undefined; + + const url = flashair.fileUrl(entry.path); + const totalBytes = entry.size; + + try { + const res = await fetch(url, { signal: abort.signal }); + if (!res.ok) { + throw new Error(`${res.status} ${res.statusText}`); + } + + const reader = res.body?.getReader(); + if (reader === undefined) { + const blob = await res.blob(); + if (abort.signal.aborted) return; + const objectUrl = URL.createObjectURL(blob); + rawObjectUrl = objectUrl; + fullObjectUrl = objectUrl; + progress = 1; + downloading = false; + return; + } + + const chunks: Uint8Array[] = []; + let received = 0; + + while (true) { + const { done, value } = await reader.read(); + if (done) break; + chunks.push(value); + received += value.byteLength; + progress = totalBytes > 0 ? received / totalBytes : 0; + } + + if (abort.signal.aborted) return; + + const blob = new Blob(chunks); + const objectUrl = URL.createObjectURL(blob); + rawObjectUrl = objectUrl; + fullObjectUrl = objectUrl; + progress = 1; + } catch (e) { + if (abort.signal.aborted) return; + loadError = e instanceof Error ? e.message : String(e); + } finally { + if (!abort.signal.aborted) { + downloading = false; + } + } + } + + let progressPercent = $derived(Math.round(progress * 100)); + let showThumbnail = $derived(fullObjectUrl === undefined && thumbnailUrl !== undefined); </script> <div class="h-full flex items-center justify-center bg-base-300 relative"> - {#if file === undefined || imageUrl === undefined} + {#if file === undefined} <div class="text-base-content/40 text-center p-8"> <p class="text-lg">Select a photo to preview</p> </div> + {:else if loadError !== undefined} + <div class="text-center p-8"> + <p class="text-error mb-2">Failed to load image</p> + <p class="text-sm text-base-content/60">{loadError}</p> + </div> {:else} - {#key file.path} - {#if !imageLoaded} - <span class="loading loading-spinner loading-lg absolute"></span> - {/if} + {#if downloading && progress < 1} + <div class="absolute inset-x-0 top-0 z-10 p-3"> + <div class="max-w-xs mx-auto"> + <div class="flex items-center gap-2"> + <progress class="progress progress-primary flex-1" value={progressPercent} max="100"></progress> + <span class="text-xs font-mono text-base-content/70 w-10 text-right">{progressPercent}%</span> + </div> + </div> + </div> + {/if} + {#if fullObjectUrl !== undefined} + {#key fullObjectUrl} + <img + src={fullObjectUrl} + alt={file.filename} + class="max-w-full max-h-full object-contain" + /> + {/key} + {:else if showThumbnail} <img - src={imageUrl} + src={thumbnailUrl} alt={file.filename} - class="max-w-full max-h-full object-contain" - class:opacity-0={!imageLoaded} - onload={() => { imageLoaded = true; }} + class="max-w-full max-h-full object-contain image-rendering-pixelated" + style="image-rendering: pixelated;" /> - {/key} + {:else} + <span class="loading loading-spinner loading-lg"></span> + {/if} {/if} </div> |
