web: deep-link from RightSidebar to folder/map + anchor-mode timeline + fix root count

RightSidebar:
- Folder + Location rows gain a small ArrowUpRight icon button that
  deep-links into the timeline / map view focused on the photo. OSM
  external link removed; the in-app map nav covers the same job.

Folder navigation:
- New navigateToFolder(path, { focusUid, focusTakenAt }) helper in the
  filters store; LeftSidebar's pickFolder collapses to a one-liner that
  reuses it.
- One-shot pending-focus stash carries both UID and TakenAt across the
  goto. URL-watch effect on the timeline consumes the stash so even
  same-folder navigations (where the filter doesn't change) get
  picked up.

Anchor-mode timeline query:
- listPhotosAround(q, takenAt, after, before) issues two parallel
  PhotoPrism calls (`after:<day-1>` oldest-first + `before:<day+1>`
  newest-first), merges + dedupes newest-first. Uses PhotoPrism's
  existing date-only DSL clauses — no server changes.
- When a deep-link stashes a TakenAt, page 0 of the photosQuery uses
  the merged window so the target photo is loaded even for photos
  buried past the standard newest-first cursor. Pages 1+ are disabled
  in anchor mode (PhotoPrism's day-precision cursor would infinite-loop
  on dense days; users see 120 around the target, refresh to drop the
  anchor).
- After page 0 lands, the existing scrollToIndex(targetIdx) expands the
  windowed render set + scrolls the tile into view.

Map view:
- /map honors `?lat=&lng=&zoom=&focus=` URL params, jumping to the
  photo's coordinates at zoom 17 instead of fitBounds-ing the full
  library. Params are stripped after first apply so a manual zoom-out
  + reload doesn't snap back.

LeftSidebar root count badge:
- Now matches what Cmd+A selects in the timeline. Old code used
  /config.count.all (library aggregate, includes archived/hidden/
  review). Switched to countPhotos('', { merged: true }) which counts
  the actual photo entries the timeline lists.
- countPhotos gains a `merged` option; with merged=true it returns the
  response body length instead of the X-Count header — PhotoPrism's
  X-Count is always the file-row count regardless of merged, so a
  HEIC + JPG companion pair inflated the badge to 2.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-20 15:04:23 +02:00
parent c134afe023
commit 29f7ad7073
7 changed files with 363 additions and 55 deletions

View File

@@ -299,6 +299,37 @@
markersOnScreen.clear();
markers.clear();
// Deep-link from the RightSidebar's location open icon: `?lat=&lng=`
// (+ optional `zoom`, `focus`) flies the map directly to the photo
// rather than fitting to the full library extent. Strip the params
// afterwards so a manual zoom-out + reload doesn't snap back. Falls
// through to the default fitBounds when the params aren't present.
const sp = new URL(window.location.href).searchParams;
const latParam = Number(sp.get('lat'));
const lngParam = Number(sp.get('lng'));
if (
(data.features?.length ?? 0) > 0 &&
Number.isFinite(latParam) &&
Number.isFinite(lngParam) &&
sp.has('lat') &&
sp.has('lng')
) {
const zoom = Number(sp.get('zoom')) || 17;
map.jumpTo({ center: [lngParam, latParam], zoom });
const stripped = new URL(window.location.href);
stripped.searchParams.delete('lat');
stripped.searchParams.delete('lng');
stripped.searchParams.delete('zoom');
stripped.searchParams.delete('focus');
const qs = stripped.searchParams.toString();
void goto(`/map${qs ? `?${qs}` : ''}`, {
replaceState: true,
keepFocus: true,
noScroll: true
});
return;
}
// Fit to data extent on the first non-empty load — prefer the
// server-provided bbox (PhotoPrism returns one), else compute from
// the features.