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:
2026-05-20 11:20:02 +02:00
parent 55c870c155
commit d1ddc48f81
15 changed files with 607 additions and 512 deletions

View File

@@ -0,0 +1,57 @@
/**
* Best-effort calendar date guessed from a photo's filename / parent
* folders. Returns `YYYY-MM-DD` only when year + month + day are all
* present and form a real date; returns `null` for year-only or
* unparseable inputs.
*
* Used by the EXIF Stripped review tab to pre-fill the metadata
* sidebar's date suggestion row.
*/
import { isValidISODate } from '$lib/services/photoprism';
interface Input {
fileName?: string;
path?: string;
}
function pad2(n: number): string {
return n < 10 ? `0${n}` : String(n);
}
function tryDate(y: number, m: number, d: number): string | null {
if (y < 1900 || y > 2100) return null;
if (m < 1 || m > 12) return null;
if (d < 1 || d > 31) return null;
const iso = `${y}-${pad2(m)}-${pad2(d)}`;
return isValidISODate(iso) ? iso : null;
}
// `YYYY[sep]MM[sep]DD` where `sep` is an optional `-` or `_`. Anchored by
// non-digit boundaries on both sides so `IMG_20070612_135421.jpg` matches
// `20070612` cleanly without bleeding into the trailing timestamp.
const BASENAME_YMD = /(?<!\d)(\d{4})[-_]?(\d{2})[-_]?(\d{2})(?!\d)/;
// Path variant accepts `/` as a separator too — `2007/06/12`, `2007-06-12/`.
const PATH_YMD = /(?<!\d)(\d{4})[-_/](\d{2})[-_/](\d{2})(?!\d)/;
export function suggestDateFromPath(input: Input): string | null {
const fileName = (input.fileName ?? '').trim();
const path = (input.path ?? '').trim();
const fnMatch = fileName.match(BASENAME_YMD);
if (fnMatch) {
const iso = tryDate(Number(fnMatch[1]), Number(fnMatch[2]), Number(fnMatch[3]));
if (iso) return iso;
}
if (path) {
const pMatch = path.match(PATH_YMD);
if (pMatch) {
const iso = tryDate(Number(pMatch[1]), Number(pMatch[2]), Number(pMatch[3]));
if (iso) return iso;
}
}
return null;
}