- 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>
105 lines
3.4 KiB
Svelte
105 lines
3.4 KiB
Svelte
<!--
|
||
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 XS–XL
|
||
* 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>
|