web(review): YYYY/MM path fallback + suggestion row below date + tighten 'a' gate
- suggestDateFromPath: when only year+month appear in the path (e.g. 2024/01/), synthesize day=01 so date-only foldering yields a usable suggestion instead of null. - RightSidebar: move the suggestion row below the Taken-at input. - BulkActionBar + gridKeyNav: show the "Accept date & Keep" button and fire the bare 'a' shortcut only when EVERY targeted photo has a path-derivable date — no more silent approve-without-fix for mixed selections. - gridKeyNav: drop local cachedPhoto duplicate, use the shared one. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -329,11 +329,25 @@
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Date suggestion derived from the file/folder path. Shown only
|
||||
when the photo's stored date is missing or untrusted (the
|
||||
`stripped_exif` heuristic). Amber styling marks it as
|
||||
unconfirmed — clicking Apply commits as a manual TakenAt
|
||||
edit. -->
|
||||
<!-- Taken at -->
|
||||
<div class="flex items-center gap-2">
|
||||
<Calendar class="h-3.5 w-3.5 shrink-0 text-muted-foreground" />
|
||||
<input
|
||||
type="text"
|
||||
inputmode="numeric"
|
||||
placeholder="YYYY-MM-DD"
|
||||
pattern="\d{4}-\d{2}-\d{2}"
|
||||
aria-invalid={!takenAtValid}
|
||||
class="min-w-0 flex-1 rounded border border-transparent bg-transparent px-1 py-0.5 text-xs hover:border-input focus:border-input focus:outline-none focus:ring-1 focus:ring-ring aria-invalid:border-destructive aria-invalid:text-destructive aria-invalid:focus:ring-destructive"
|
||||
bind:value={takenAt}
|
||||
onblur={commitTakenAt}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Date suggestion derived from the file/folder path. Only shown
|
||||
on the EXIF Stripped review tab; amber styling marks it as
|
||||
unconfirmed. Apply writes the value into the date input above
|
||||
and commits as a manual TakenAt edit. -->
|
||||
{#if showDateSuggestion}
|
||||
<div
|
||||
class="flex items-center gap-2 rounded border border-amber-300/70 bg-amber-50/40 px-1.5 py-1 text-[11px] text-amber-700 dark:border-amber-500/40 dark:bg-amber-500/10 dark:text-amber-300"
|
||||
@@ -352,21 +366,6 @@
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Taken at -->
|
||||
<div class="flex items-center gap-2">
|
||||
<Calendar class="h-3.5 w-3.5 shrink-0 text-muted-foreground" />
|
||||
<input
|
||||
type="text"
|
||||
inputmode="numeric"
|
||||
placeholder="YYYY-MM-DD"
|
||||
pattern="\d{4}-\d{2}-\d{2}"
|
||||
aria-invalid={!takenAtValid}
|
||||
class="min-w-0 flex-1 rounded border border-transparent bg-transparent px-1 py-0.5 text-xs hover:border-input focus:border-input focus:outline-none focus:ring-1 focus:ring-ring aria-invalid:border-destructive aria-invalid:text-destructive aria-invalid:focus:ring-destructive"
|
||||
bind:value={takenAt}
|
||||
onblur={commitTakenAt}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Folder (read-only). The `px-1 py-0.5` mirrors the input
|
||||
padding on filename / date so the read-only text starts at the
|
||||
same x-offset as the editable rows above — otherwise spans
|
||||
|
||||
@@ -65,7 +65,12 @@
|
||||
const onExifStrippedTab = $derived(
|
||||
isReview && page.url.searchParams.get('tab') === 'stripped_exif'
|
||||
);
|
||||
const hasAnySuggestion = $derived.by(() => {
|
||||
// Surface the button only when EVERY targeted photo has a derivable
|
||||
// suggestion — otherwise clicking it would silently approve some
|
||||
// photos without a date fix, which contradicts the verb. A uid not in
|
||||
// any cache also counts as "no suggestion" so we don't promise
|
||||
// something we can't verify.
|
||||
const allHaveSuggestion = $derived.by(() => {
|
||||
if (!onExifStrippedTab) return false;
|
||||
const ids =
|
||||
selection.ids.size > 0
|
||||
@@ -73,13 +78,13 @@
|
||||
: selection.focused
|
||||
? [selection.focused]
|
||||
: [];
|
||||
if (ids.length === 0) return false;
|
||||
for (const id of ids) {
|
||||
const p = cachedPhoto(id);
|
||||
if (p && suggestDateFromPath({ fileName: p.FileName, path: p.Path })) {
|
||||
return true;
|
||||
}
|
||||
if (!p) return false;
|
||||
if (!suggestDateFromPath({ fileName: p.FileName, path: p.Path })) return false;
|
||||
}
|
||||
return false;
|
||||
return true;
|
||||
});
|
||||
// Archive section is the parallel two-button flow: Keep (restore back
|
||||
// to the timeline) or Delete (permanent, no undo). X is repurposed
|
||||
@@ -271,12 +276,11 @@
|
||||
✓ Keep
|
||||
<kbd class="rounded bg-muted px-1 text-[9px] font-medium text-muted-foreground">S</kbd>
|
||||
</button>
|
||||
{#if hasAnySuggestion}
|
||||
<!-- Surfaces only when at least one selected photo has a
|
||||
derivable date from its path AND lacks a trusted
|
||||
TakenAt. Applies the suggested date patch then
|
||||
approves in one go; uids without a usable suggestion
|
||||
are just approved. -->
|
||||
{#if allHaveSuggestion}
|
||||
<!-- Visible only when every selected photo has a path-
|
||||
derivable date. Clicking applies each photo's
|
||||
suggestion then approves it; mirrored by the bare
|
||||
`a` shortcut in gridKeyNav. -->
|
||||
<button
|
||||
class="inline-flex items-center gap-1 rounded border border-amber-400/60 bg-amber-100/40 px-2 py-0.5 text-[11px] text-amber-800 hover:bg-amber-100 disabled:opacity-50 dark:border-amber-400/40 dark:bg-amber-500/15 dark:text-amber-200 dark:hover:bg-amber-500/25"
|
||||
disabled={busy}
|
||||
|
||||
Reference in New Issue
Block a user