diff --git a/web/src/lib/components/preview/InlinePreview.svelte b/web/src/lib/components/preview/InlinePreview.svelte index 0bcb338..e9ca698 100644 --- a/web/src/lib/components/preview/InlinePreview.svelte +++ b/web/src/lib/components/preview/InlinePreview.svelte @@ -8,7 +8,8 @@ import { createQuery } from '@tanstack/svelte-query'; import { getPhoto } from '$lib/services/photoprism'; import { thumbUrl, videoUrl } from '$lib/stores/session.svelte'; - import { setAnchor, setFocused } from '$lib/stores/selection.svelte'; + import { selection, setAnchor, setFocused } from '$lib/stores/selection.svelte'; + import SelectionDeck from '$lib/components/preview/SelectionDeck.svelte'; import VideoPlayer from '$lib/components/preview/VideoPlayer.svelte'; import { isVideo, primaryFile, videoFile, type PpPhoto } from '$lib/types/photoprism'; @@ -21,6 +22,14 @@ } let { uid, order }: Props = $props(); + // Multi-select swaps the single-photo pane for a fanned deck of the + // selected thumbnails. Reads `selection.ids` directly (already in + // scope via the import) so the host doesn't need to thread it + // through as a prop. Spreading SvelteSet preserves insertion order, + // so the most recently picked card sits on top of the fan. + const selectedUids = $derived([...selection.ids]); + const multiSelect = $derived(selectedUids.length >= 2); + const photoQuery = createQuery(() => ({ queryKey: ['photo', uid ?? ''], queryFn: () => getPhoto(uid as string), @@ -38,7 +47,9 @@
- {#if uid === null} + {#if multiSelect} + + {:else if uid === null}

Select a photo to preview.

{:else if photoQuery.isPending}

Loading…

diff --git a/web/src/lib/components/preview/SelectionDeck.svelte b/web/src/lib/components/preview/SelectionDeck.svelte new file mode 100644 index 0000000..922351c --- /dev/null +++ b/web/src/lib/components/preview/SelectionDeck.svelte @@ -0,0 +1,112 @@ + + + +
+ {#if cards.length === 0} +

Selection has no resolvable thumbnails yet.

+ {/if} + {#each cards as card, i (card.uid)} + + {/each} + + {#if cards.length > 0} +
+ {cards.length} selected +
+ {/if} +
diff --git a/web/src/lib/components/timeline/PhotoGrid.svelte b/web/src/lib/components/timeline/PhotoGrid.svelte index 88cf453..dbaf47c 100644 --- a/web/src/lib/components/timeline/PhotoGrid.svelte +++ b/web/src/lib/components/timeline/PhotoGrid.svelte @@ -81,13 +81,6 @@ setFocused(uid); setAnchor(uid); } - - function onOpenPreview(uid: string) { - selection.ids.clear(); - selection.ids.add(uid); - setFocused(uid); - setAnchor(uid); - }
@@ -99,7 +92,6 @@ selected={sel} onClick={(e) => onClick(e, photo.UID)} onDblclick={(e) => onDblclick(e, photo.UID)} - onOpenPreview={() => onOpenPreview(photo.UID)} />
{/each} diff --git a/web/src/lib/components/timeline/PhotoTile.svelte b/web/src/lib/components/timeline/PhotoTile.svelte index 3cf4102..c23f293 100644 --- a/web/src/lib/components/timeline/PhotoTile.svelte +++ b/web/src/lib/components/timeline/PhotoTile.svelte @@ -2,20 +2,17 @@ Single photo tile — shared by the timeline (+page.svelte) and the flat drill-in grids (PhotoGrid.svelte) so the tile chrome is single-sourced and can't drift between the two. Owns: thumbnail, selection animation, - video badge, and the hover-only "open preview" affordance. + and the video badge. Sizing is delegated to the caller — PhotoTile fills its container with h-full w-full, so the timeline can wrap it in its windowing shell and PhotoGrid can wrap it in an aspect-square cell. Click semantics: - - plain click → onClick (caller decides; both surfaces use it for select-only) - - dblclick → onDblclick (caller usually opens preview) - - Maximize ico → onOpenPreview (single-click fallback for the - not-yet-discovered dblclick gesture) + - plain click → onClick (caller decides; both surfaces use it for select-only) + - dblclick → onDblclick (caller usually opens preview) -->
{/if} - - {#if !selected} - - {/if}
diff --git a/web/src/routes/+page.svelte b/web/src/routes/+page.svelte index dd9e74c..f7d2e7e 100644 --- a/web/src/routes/+page.svelte +++ b/web/src/routes/+page.svelte @@ -665,13 +665,6 @@ setAnchor(uid); } - function onTileOpenPreview(uid: string) { - selection.ids.clear(); - selection.ids.add(uid); - setFocused(uid); - setAnchor(uid); - } - // Scroll root for the infinite-scroll IntersectionObserver. Bound by // the
element below; the sentinel's `root` references this so // the observer measures intersections relative to the timeline pane @@ -905,7 +898,6 @@ selected={sel} onClick={(e) => onTileClick(e, photo.UID)} onDblclick={(e) => onTileDblclick(e, photo.UID)} - onOpenPreview={() => onTileOpenPreview(photo.UID)} /> {/if}