diff options
Diffstat (limited to 'src/lib')
| -rw-r--r-- | src/lib/components/ImageList.svelte | 52 | ||||
| -rw-r--r-- | src/lib/components/ImagePreview.svelte | 40 | ||||
| -rw-r--r-- | src/lib/flashair/client.ts | 39 |
3 files changed, 131 insertions, 0 deletions
diff --git a/src/lib/components/ImageList.svelte b/src/lib/components/ImageList.svelte new file mode 100644 index 0000000..7e6f836 --- /dev/null +++ b/src/lib/components/ImageList.svelte @@ -0,0 +1,52 @@ +<script lang="ts"> + import { flashair } from '../flashair'; + import type { FlashAirFileEntry } from '../flashair'; + + interface Props { + images: FlashAirFileEntry[]; + selectedPath: string | undefined; + onSelect: (file: FlashAirFileEntry) => void; + } + + let { images, selectedPath, onSelect }: Props = $props(); +</script> + +<div class="h-full overflow-y-auto bg-base-100"> + {#if images.length === 0} + <div class="flex items-center justify-center h-full text-base-content/50 p-4"> + <p class="text-center text-sm">No images found</p> + </div> + {:else} + <ul class="flex flex-col gap-0.5 p-1"> + {#each images as file (file.path)} + {@const thumbUrl = flashair.thumbnailUrl(file.path)} + {@const isSelected = file.path === selectedPath} + <li> + <button + class="flex items-center gap-2 w-full rounded-lg p-1.5 text-left transition-colors {isSelected + ? 'bg-primary text-primary-content' + : 'hover:bg-base-200'}" + onclick={() => onSelect(file)} + > + {#if thumbUrl} + <img + src={thumbUrl} + alt={file.filename} + class="w-12 h-12 rounded object-cover shrink-0" + loading="lazy" + /> + {:else} + <div class="w-12 h-12 rounded bg-base-300 flex items-center justify-center shrink-0"> + <span class="text-lg">🖼️</span> + </div> + {/if} + <div class="min-w-0 flex-1"> + <p class="text-xs font-medium truncate">{file.filename}</p> + <p class="text-xs opacity-60">{file.date.toLocaleString()}</p> + </div> + </button> + </li> + {/each} + </ul> + {/if} +</div> diff --git a/src/lib/components/ImagePreview.svelte b/src/lib/components/ImagePreview.svelte new file mode 100644 index 0000000..82115e7 --- /dev/null +++ b/src/lib/components/ImagePreview.svelte @@ -0,0 +1,40 @@ +<script lang="ts"> + import { flashair } from '../flashair'; + import type { FlashAirFileEntry } from '../flashair'; + + interface Props { + file: FlashAirFileEntry | undefined; + } + + let { file }: Props = $props(); + + let imageUrl = $derived(file !== undefined ? flashair.fileUrl(file.path) : undefined); + let imageLoaded = $state(false); + + $effect(() => { + if (imageUrl !== undefined) { + imageLoaded = false; + } + }); +</script> + +<div class="h-full flex items-center justify-center bg-base-300 relative"> + {#if file === undefined || imageUrl === undefined} + <div class="text-base-content/40 text-center p-8"> + <p class="text-lg">Select a photo to preview</p> + </div> + {:else} + {#key file.path} + {#if !imageLoaded} + <span class="loading loading-spinner loading-lg absolute"></span> + {/if} + <img + src={imageUrl} + alt={file.filename} + class="max-w-full max-h-full object-contain" + class:opacity-0={!imageLoaded} + onload={() => { imageLoaded = true; }} + /> + {/key} + {/if} +</div> diff --git a/src/lib/flashair/client.ts b/src/lib/flashair/client.ts index 4e3a567..978eb60 100644 --- a/src/lib/flashair/client.ts +++ b/src/lib/flashair/client.ts @@ -77,6 +77,9 @@ function parseFileList(text: string): FlashAirFileEntry[] { /** Known image extensions the FlashAir thumbnail.cgi supports (JPEG only). */ const JPEG_EXTENSIONS = new Set(['.jpg', '.jpeg']); +/** Regex matching common image file extensions. */ +const IMAGE_EXTENSIONS = /\.(jpe?g|png|bmp|gif)$/i; + function isJpeg(filename: string): boolean { const ext = filename.slice(filename.lastIndexOf('.')).toLowerCase(); return JPEG_EXTENSIONS.has(ext); @@ -193,6 +196,42 @@ export const flashair = { return { blob, meta }; }, + /** + * Recursively list all image files on the card starting from a root directory. + * Returns files sorted by date, newest first. + */ + async listAllImages(rootDir: string): Promise<FlashAirFileEntry[]> { + const allImages: FlashAirFileEntry[] = []; + const dirsToVisit: string[] = [rootDir]; + + while (dirsToVisit.length > 0) { + const dir = dirsToVisit.pop(); + if (dir === undefined) break; + + let entries: FlashAirFileEntry[]; + try { + entries = await flashair.listFiles(dir); + } catch { + continue; + } + + for (const entry of entries) { + if (entry.isDirectory) { + dirsToVisit.push(entry.path); + } else if (IMAGE_EXTENSIONS.test(entry.filename)) { + allImages.push(entry); + } + } + } + + allImages.sort((a, b) => { + const dateCompare = b.rawDate - a.rawDate; + if (dateCompare !== 0) return dateCompare; + return b.rawTime - a.rawTime; + }); + + return allImages; + }, } as const; export type { FlashAirFileEntry, ThumbnailMeta } from './types'; |
