diff --git a/web/src/lib/actions/gridKeyNav.ts b/web/src/lib/actions/gridKeyNav.ts index 9614c80..c890e4f 100644 --- a/web/src/lib/actions/gridKeyNav.ts +++ b/web/src/lib/actions/gridKeyNav.ts @@ -10,7 +10,8 @@ import { removeFromHeap, type PpAlbum } from '$lib/services/photoprism'; -import { acceptDateAndKeep } from '$lib/services/photoActions'; +import { acceptDateAndKeep, cachedPhoto } from '$lib/services/photoActions'; +import { suggestDateFromPath } from '$lib/utils/suggestDateFromPath'; import { queryClient } from '$lib/queryClient'; import { filters } from '$lib/stores/filters.svelte'; import { @@ -26,7 +27,6 @@ import { } from '$lib/stores/selection.svelte'; import { popAndRun, push as pushUndo } from '$lib/stores/undo.svelte'; import { openPreview, toggleLeftSidebar, toggleRightSidebar, view } from '$lib/stores/view.svelte'; -import type { PpPhoto } from '$lib/types/photoprism'; /** * Optional parameters the host passes via `use:gridKeyNav={...}`. @@ -161,35 +161,6 @@ export function gridKeyNav(node: HTMLElement, params: GridKeyNavParams = {}) { return []; } - /** Look up a photo's current cached state without forcing a refetch. - * Walks every `['photos', …]` cache entry first, then the per-photo - * cache. Lets `x` decide "archive vs restore" based on the actual current - * state instead of always sending Archived=true. - * - * The `['photos', …]` namespace holds two shapes: a flat `PpPhoto[]` - * (e.g. ratings/colors pools) and TanStack's `InfiniteData` envelope - * (`{pages: PpPhoto[][], pageParams}`) used by the timeline's infinite - * scroll. Walk both — assuming a flat array on the timeline cache used - * to throw `list.find is not a function` and abort the F/X handlers. */ - function cachedPhoto(uid: string): PpPhoto | undefined { - const lists = queryClient.getQueriesData({ queryKey: ['photos'] }); - for (const [, data] of lists) { - if (!data) continue; - if (Array.isArray(data)) { - const hit = (data as PpPhoto[]).find((p) => p.UID === uid); - if (hit) return hit; - continue; - } - const pages = (data as { pages?: PpPhoto[][] }).pages; - if (!Array.isArray(pages)) continue; - for (const page of pages) { - const hit = page?.find?.((p) => p.UID === uid); - if (hit) return hit; - } - } - return queryClient.getQueryData(['photo', uid]); - } - async function toggleArchive(direction: 'archive' | 'restore' | 'toggle') { const ids = cullTargets(); if (ids.length === 0) { @@ -503,25 +474,21 @@ export function gridKeyNav(node: HTMLElement, params: GridKeyNavParams = {}) { } if (shift) return; // Bare `a` on the EXIF Stripped review tab fires the same - // "Accept date & Keep" flow the bar button uses. Scoped to - // that tab so the key stays free everywhere else (where a - // path-derived date wouldn't make sense as a one-shot - // shortcut). Reads location directly because actions sit - // outside the component tree where `$app/state` is - // idiomatic — same approach used by `filters.section` - // elsewhere in this file. + // "Accept date & Keep" flow as the bar button. Mirrors the + // bar's all-targets-have-a-suggestion gate so the shortcut + // can't silently approve photos without a date fix. if ( filters.section === 'review' && new URL(window.location.href).searchParams.get('tab') === 'stripped_exif' ) { - e.preventDefault(); const ids = cullTargets(); - if (ids.length === 0) { - toast.message('Nothing to keep', { - description: 'Click a photo or select some first' - }); - return; + if (ids.length === 0) return; + for (const id of ids) { + const p = cachedPhoto(id); + if (!p) return; + if (!suggestDateFromPath({ fileName: p.FileName, path: p.Path })) return; } + e.preventDefault(); void acceptDateAndKeep(ids); } return; diff --git a/web/src/lib/components/sidebar/RightSidebar.svelte b/web/src/lib/components/sidebar/RightSidebar.svelte index b7dfa93..6b2c11a 100644 --- a/web/src/lib/components/sidebar/RightSidebar.svelte +++ b/web/src/lib/components/sidebar/RightSidebar.svelte @@ -329,11 +329,25 @@ {/if} - + +
+ + +
+ + {#if showDateSuggestion}
{/if} - -
- - -
- + {#if allHaveSuggestion} +