web(review): switch cause tabs to PhotoGrid, add date-from-path suggestion
- CauseGroupCard now wraps PhotoGrid so selection, keyboard nav, and preview flow through the standard timeline plumbing. Per-tile hover Approve/Archive and the group-wide Approve all are gone — the bottom BulkActionBar's review-section Keep/Archive handle single + bulk. - Low Resolution tab opts into a new PhotoTile dimensionBadge prop so WxH stays visible on each tile. - New suggestDateFromPath util parses YYYY-MM-DD from filename or folder path. RightSidebar surfaces it as an amber Apply row above the Taken-at input whenever the photo lacks a trusted TakenAt. - BulkActionBar gains a "Accept date & Keep" button (review section only) that patches each selected photo's TakenAt from its path suggestion when available, then approves. - Drop the Same folder / Same camera / Same year strips and the RelatedStrip component from the metadata sidebar. Also bundles in-progress Notes route + tile components and small tweaks to LeftSidebar, DuplicatesView, CrossFolderGroupCard, and photoprism.ts. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -39,15 +39,12 @@
|
||||
import { getMetadataSectionOpen, setMetadataSection } from '$lib/stores/view.svelte';
|
||||
import { primaryFile, type PpPhoto } from '$lib/types/photoprism';
|
||||
import { COLOR_SWATCHES } from '$lib/utils/tagGroups';
|
||||
import RelatedStrip from './RelatedStrip.svelte';
|
||||
import { suggestDateFromPath } from '$lib/utils/suggestDateFromPath';
|
||||
|
||||
interface Props {
|
||||
/** When true, append related-photo strips (folder/camera/year) below
|
||||
* Keywords. Used by the /review route; left off on the timeline. */
|
||||
showRelated?: boolean;
|
||||
photo: PpPhoto;
|
||||
}
|
||||
let { photo, showRelated = false }: Props = $props();
|
||||
let { photo }: Props = $props();
|
||||
|
||||
const qc = useQueryClient();
|
||||
|
||||
@@ -131,6 +128,25 @@
|
||||
commit({ Caption: caption, CaptionSrc: 'manual' });
|
||||
}
|
||||
const takenAtValid = $derived(takenAt === '' || isValidISODate(takenAt));
|
||||
// Path-based date guess. Surfaced only when the photo's stored date is
|
||||
// missing or untrusted — same heuristic the review adapter uses to
|
||||
// bucket photos into `stripped_exif`. Tying it to the heuristic rather
|
||||
// than a specific tab means the suggestion shows up wherever the user
|
||||
// lands on a date-less photo (timeline drill-in, archive, etc.).
|
||||
const needsDate = $derived(
|
||||
!photo.TakenSrc || photo.TakenSrc === 'name' || !photo.TakenAt
|
||||
);
|
||||
const dateSuggestion = $derived(
|
||||
suggestDateFromPath({ fileName: photo.FileName, path: photo.Path })
|
||||
);
|
||||
const showDateSuggestion = $derived(
|
||||
needsDate && !!dateSuggestion && dateSuggestion !== takenAt
|
||||
);
|
||||
function applyDateSuggestion() {
|
||||
if (!dateSuggestion) return;
|
||||
takenAt = dateSuggestion;
|
||||
commitTakenAt();
|
||||
}
|
||||
function commitTakenAt() {
|
||||
if (!takenAt) return;
|
||||
if (!isValidISODate(takenAt)) {
|
||||
@@ -310,6 +326,29 @@
|
||||
{/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. -->
|
||||
{#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"
|
||||
>
|
||||
<Folder class="h-3.5 w-3.5 shrink-0" />
|
||||
<span class="min-w-0 flex-1 truncate">
|
||||
Suggested from path: <span class="font-medium">{dateSuggestion}</span>
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
class="shrink-0 rounded border border-amber-400/60 bg-amber-100/60 px-1.5 py-0.5 text-[10px] font-medium text-amber-800 hover:bg-amber-100 dark:border-amber-400/30 dark:bg-amber-500/20 dark:text-amber-200 dark:hover:bg-amber-500/30"
|
||||
onclick={applyDateSuggestion}
|
||||
>
|
||||
Apply
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Taken at -->
|
||||
<div class="flex items-center gap-2">
|
||||
<Calendar class="h-3.5 w-3.5 shrink-0 text-muted-foreground" />
|
||||
@@ -373,25 +412,14 @@
|
||||
</div>
|
||||
</dl>
|
||||
|
||||
<!-- Note (PhotoPrism's Caption field — labelled "Note" to match
|
||||
mule-image's nomenclature). -->
|
||||
<div class="space-y-1">
|
||||
<div class="text-[10px] uppercase tracking-wide text-muted-foreground">Note</div>
|
||||
<textarea
|
||||
rows="2"
|
||||
placeholder="Add a note…"
|
||||
class="w-full resize-y rounded border border-input bg-background px-1.5 py-1 text-xs shadow-sm focus:outline-none focus:ring-1 focus:ring-ring"
|
||||
bind:value={caption}
|
||||
onblur={commitCaption}
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<!-- Tags — score, color label, keywords, and auto-labels grouped under
|
||||
one collapsible section. Score + color are stored on the mule-
|
||||
sidecar (PhotoPrism's PUT can't persist them); keywords live on
|
||||
Details; auto-labels come from PhotoPrism's TF classifier and are
|
||||
read-only. Open by default since these are the culling marks the
|
||||
user reaches for first. -->
|
||||
<!-- Tags — note, score, color label, keywords, and auto-labels grouped
|
||||
under one collapsible section. Note (PhotoPrism's Caption field,
|
||||
labelled here to match mule-image's nomenclature) sits at the top
|
||||
of the group since it's the most-edited per-photo field. Score +
|
||||
color are stored on the mule-sidecar (PhotoPrism's PUT can't
|
||||
persist them); keywords live on Details; auto-labels come from
|
||||
PhotoPrism's TF classifier and are read-only. Open by default
|
||||
since these are the culling marks the user reaches for first. -->
|
||||
<details
|
||||
class="rounded border border-border"
|
||||
open={getMetadataSectionOpen('tags', true)}
|
||||
@@ -405,6 +433,17 @@
|
||||
</span>
|
||||
</summary>
|
||||
<div class="space-y-2 p-2 pt-1">
|
||||
<div class="space-y-1">
|
||||
<div class="text-[10px] uppercase tracking-wide text-muted-foreground">Note</div>
|
||||
<textarea
|
||||
rows="2"
|
||||
placeholder="Add a note…"
|
||||
class="w-full resize-y rounded border border-input bg-background px-1.5 py-1 text-xs shadow-sm focus:outline-none focus:ring-1 focus:ring-ring"
|
||||
bind:value={caption}
|
||||
onblur={commitCaption}
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<div class="space-y-1">
|
||||
<div class="text-[10px] uppercase tracking-wide text-muted-foreground">Score</div>
|
||||
<div class="flex items-center gap-0.5" role="group" aria-label="Rating">
|
||||
@@ -504,35 +543,6 @@
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<!-- Related strips (only the /review route opts in). The three
|
||||
scopes match the three decisions the user usually makes here:
|
||||
"did all these come from the same shoot?" (folder), "same
|
||||
camera, EXIF-stripped together?" (camera), "right year?"
|
||||
(year). Strips with zero hits collapse silently. -->
|
||||
{#if showRelated}
|
||||
<div class="space-y-2 border-t border-border pt-2">
|
||||
<RelatedStrip
|
||||
title="Same folder"
|
||||
q={`path:"${photo.Path ?? ''}"`}
|
||||
excludeUid={photo.UID}
|
||||
/>
|
||||
{#if photo.CameraID && photo.CameraID !== 1}
|
||||
<RelatedStrip
|
||||
title="Same camera"
|
||||
q={`camera:${photo.CameraID}`}
|
||||
excludeUid={photo.UID}
|
||||
/>
|
||||
{/if}
|
||||
{#if photo.Year}
|
||||
<RelatedStrip
|
||||
title="Same year"
|
||||
q={`year:${photo.Year}`}
|
||||
excludeUid={photo.UID}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- GPS detail. Static default (closed); user's expand/collapse
|
||||
choice persists across photo switches via the view store.
|
||||
Avoid data-driven defaults here — they make the `open` attr
|
||||
|
||||
Reference in New Issue
Block a user