summaryrefslogtreecommitdiffhomepage
path: root/src/lib
diff options
context:
space:
mode:
Diffstat (limited to 'src/lib')
-rw-r--r--src/lib/components/ImageList.svelte52
-rw-r--r--src/lib/components/ImagePreview.svelte40
-rw-r--r--src/lib/flashair/client.ts39
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';