Files
mule-image/web/src/lib/components/timeline/PhotoGrid.svelte
dtoro d1ddc48f81 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>
2026-05-20 11:20:02 +02:00

105 lines
3.4 KiB
Svelte
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!--
Flat photo grid for views that don't need infinite-scroll windowing or
month headers — the drill-in screens in /tags. Wears the same tile look
+ click semantics as the timeline via the shared PhotoTile so the user
gets selection rings, single-click select, dblclick preview, hover-only
open affordance, and arrow-key nav (via `gridKeyNav` on the scroll-root)
without per-route plumbing.
The grid carries `data-photo-grid` so gridKeyNav can measure its column
count, and each tile (rendered by PhotoTile) carries `data-tile`+
`data-uid` so the action's document-level click handler can pick up
shift/cmd/ctrl modifiers and route them through the shared selection
helpers.
-->
<script lang="ts">
import { untrack } from 'svelte';
import {
isSelected,
selection,
setAnchor,
setFocused,
setOrder
} from '$lib/stores/selection.svelte';
import { openPreview, view } from '$lib/stores/view.svelte';
import { type PpPhoto } from '$lib/types/photoprism';
import PhotoTile from './PhotoTile.svelte';
interface Props {
photos: PpPhoto[];
/** Override the column template. Defaults to the global
* `view.thumbnailSize` so drill-in grids honour the same XSXL
* preset the timeline uses. */
columns?: string;
/** Forwarded to every PhotoTile. The Low Resolution review tab
* opts in so users can spot pixel dimensions without opening
* each tile. */
dimensionBadge?: boolean;
}
let { photos, columns, dimensionBadge = false }: Props = $props();
const tracks = $derived(
columns ?? `repeat(auto-fill, minmax(${view.thumbnailSize}px, 1fr))`
);
const order = $derived(photos.map((p) => p.UID));
$effect(() => {
setOrder(order);
// Re-anchor focus when the previously focused photo isn't part of
// this grid — covers drilling into a /tags category from any
// other view, where the selection.focused module state would
// otherwise leak across surfaces and the new grid would render
// with no tile highlighted. Crucially we only re-anchor when the
// uid is *absent*, so refetches that keep the focused photo
// around (e.g. after `focusAfter` set the next photo on archive)
// don't snap focus back to photos[0].
untrack(() => {
if (order.length === 0) {
setFocused(null);
selection.ids.clear();
return;
}
const cur = selection.focused;
if (cur && order.includes(cur)) return;
setFocused(order[0]);
setAnchor(order[0]);
selection.ids.clear();
});
});
function onClick(e: MouseEvent, uid: string) {
// Modifier clicks bubble to gridKeyNav's window handler (range +
// toggle paths). Plain clicks reduce the selection to this tile,
// matching the timeline's selectOnly semantics.
if (e.shiftKey || e.metaKey || e.ctrlKey) return;
selection.ids.clear();
selection.ids.add(uid);
setFocused(uid);
setAnchor(uid);
}
function onDblclick(e: MouseEvent, uid: string) {
if (e.shiftKey || e.metaKey || e.ctrlKey) return;
e.preventDefault();
selection.ids.clear();
selection.ids.add(uid);
setFocused(uid);
setAnchor(uid);
openPreview();
}
</script>
<div data-photo-grid class="grid gap-2" style="grid-template-columns: {tracks};">
{#each photos as photo (photo.UID)}
{@const sel = isSelected(photo.UID) || selection.focused === photo.UID}
<div class="aspect-square">
<PhotoTile
{photo}
selected={sel}
{dimensionBadge}
onClick={(e) => onClick(e, photo.UID)}
onDblclick={(e) => onDblclick(e, photo.UID)}
/>
</div>
{/each}
</div>