diff --git a/web/src/lib/actions/gridKeyNav.ts b/web/src/lib/actions/gridKeyNav.ts index 95fc936..274626a 100644 --- a/web/src/lib/actions/gridKeyNav.ts +++ b/web/src/lib/actions/gridKeyNav.ts @@ -17,6 +17,7 @@ import { filters } from '$lib/stores/filters.svelte'; import { closePreview, openPreview, preview } from '$lib/stores/preview.svelte'; import { clearSelection, + focusAfter, indexOf, selectRange, selection, @@ -218,6 +219,12 @@ export function gridKeyNav(node: HTMLElement, params: GridKeyNavParams = {}) { toast.error(err instanceof Error ? err.message : 'Archive failed'); return; } + // Move focus forward before the photos query refetches, so the + // user can keep X-ing through the timeline without their cursor + // snapping back to photo[0]. Walks past every uid we just + // archived/restored — relevant when the cull targets came from a + // multi-selection rather than the single focused tile. + focusAfter(ids); invalidatePhotos(ids); const label = target ? `Archived ${ids.length}` : `Restored ${ids.length}`; toast.success(label); @@ -344,12 +351,27 @@ export function gridKeyNav(node: HTMLElement, params: GridKeyNavParams = {}) { return; } try { - await addToHeap(heap.UID, ids); + const { added } = await addToHeap(heap.UID, ids); void queryClient.invalidateQueries({ queryKey: ['heaps'] }); void queryClient.invalidateQueries({ queryKey: ['photos'] }); - toast.success(`Added ${ids.length} → ${heap.Title}`); - pushUndo(`Added ${ids.length} to ${heap.Title}`, async () => { - await removeFromHeap(heap.UID, ids); + // PhotoPrism returns 200 even when nothing was added — distinguish + // "really added N" from "skipped all N" so the toast tells the + // truth. + if (added.length === 0) { + toast.error(`Nothing added to ${heap.Title}`, { + description: `PhotoPrism rejected all ${ids.length} UIDs (already in heap, or not indexed).` + }); + return; + } + if (added.length < ids.length) { + toast.success(`Added ${added.length}/${ids.length} → ${heap.Title}`, { + description: 'The rest were already in this heap.' + }); + } else { + toast.success(`Added ${added.length} → ${heap.Title}`); + } + pushUndo(`Added ${added.length} to ${heap.Title}`, async () => { + await removeFromHeap(heap.UID, added); void queryClient.invalidateQueries({ queryKey: ['heaps'] }); void queryClient.invalidateQueries({ queryKey: ['photos'] }); }); diff --git a/web/src/lib/components/sidebar/BulkMetadataSidebar.svelte b/web/src/lib/components/sidebar/BulkMetadataSidebar.svelte index 8084be6..b6a07c6 100644 --- a/web/src/lib/components/sidebar/BulkMetadataSidebar.svelte +++ b/web/src/lib/components/sidebar/BulkMetadataSidebar.svelte @@ -119,10 +119,10 @@ } const COLOR_SWATCHES: { key: string; bg: string; title: string }[] = [ - { key: 'red', bg: 'bg-red-500', title: 'Red' }, - { key: 'orange', bg: 'bg-orange-500', title: 'Orange' }, - { key: 'yellow', bg: 'bg-yellow-400', title: 'Yellow' }, - { key: 'green', bg: 'bg-green-500', title: 'Green' } + { key: 'red', bg: 'bg-red-500', title: 'Red — reject' }, + { key: 'orange', bg: 'bg-orange-500', title: 'Orange — review' }, + { key: 'yellow', bg: 'bg-yellow-400', title: 'Yellow — pick' }, + { key: 'green', bg: 'bg-green-500', title: 'Green — keep' } ]; async function applyKeyword() { diff --git a/web/src/lib/components/sidebar/RightSidebar.svelte b/web/src/lib/components/sidebar/RightSidebar.svelte index 2e6b0e1..894276a 100644 --- a/web/src/lib/components/sidebar/RightSidebar.svelte +++ b/web/src/lib/components/sidebar/RightSidebar.svelte @@ -15,6 +15,7 @@ ExternalLink, Heart, ImageIcon, + Loader2, Lock, MapPin, Star, @@ -244,11 +245,14 @@ void applyMark({ color: value }); } + // Tooltips follow the Lightroom culling convention so the swatches + // read as actions, not just colors. Red = reject, Yellow = pick, + // Green = keep, Orange = review-later. const COLOR_SWATCHES: { key: string; bg: string; title: string }[] = [ - { key: 'red', bg: 'bg-red-500', title: 'Red' }, - { key: 'orange', bg: 'bg-orange-500', title: 'Orange' }, - { key: 'yellow', bg: 'bg-yellow-400', title: 'Yellow' }, - { key: 'green', bg: 'bg-green-500', title: 'Green' } + { key: 'red', bg: 'bg-red-500', title: 'Red — reject' }, + { key: 'orange', bg: 'bg-orange-500', title: 'Orange — review' }, + { key: 'yellow', bg: 'bg-yellow-400', title: 'Yellow — pick' }, + { key: 'green', bg: 'bg-green-500', title: 'Green — keep' } ]; const photoMark = $derived(marksQuery.data?.[photo.UID] ?? {}); @@ -314,6 +318,11 @@ onkeydown={(e) => e.key === 'Enter' && (e.currentTarget as HTMLInputElement).blur()} title={renaming ? 'Renaming…' : 'Click to rename file on disk'} /> + + {#if renaming} + + {/if} {/if} -
+ + {/each} + + {/if}
{#snippet trailing()} diff --git a/web/src/routes/tags/+page.svelte b/web/src/routes/tags/+page.svelte index f51d6c7..a0b17f7 100644 --- a/web/src/routes/tags/+page.svelte +++ b/web/src/routes/tags/+page.svelte @@ -138,11 +138,15 @@ return out; } + // Titles follow the Lightroom culling convention so users see the + // swatch's *intent* (reject/review/pick/keep), not just its color. + // Used both as tooltip on swatches and as the visible card label in + // the colors-tab picker grid below. const COLOR_SWATCHES: { key: string; bg: string; title: string }[] = [ - { key: 'red', bg: 'bg-red-500', title: 'Red' }, - { key: 'orange', bg: 'bg-orange-500', title: 'Orange' }, - { key: 'yellow', bg: 'bg-yellow-400', title: 'Yellow' }, - { key: 'green', bg: 'bg-green-500', title: 'Green' } + { key: 'red', bg: 'bg-red-500', title: 'Red — reject' }, + { key: 'orange', bg: 'bg-orange-500', title: 'Orange — review' }, + { key: 'yellow', bg: 'bg-yellow-400', title: 'Yellow — pick' }, + { key: 'green', bg: 'bg-green-500', title: 'Green — keep' } ]; interface ColorGroup { key: string;