From 29f7ad70734111baadc60744fb57a2237ff79fa2 Mon Sep 17 00:00:00 2001 From: dtoro Date: Wed, 20 May 2026 15:04:23 +0200 Subject: [PATCH] web: deep-link from RightSidebar to folder/map + anchor-mode timeline + fix root count MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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:` oldest-first + `before:` 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) --- .../lib/components/layout/LeftSidebar.svelte | 43 +++++--- .../components/sidebar/RightSidebar.svelte | 61 +++++++---- .../components/timeline/BulkActionBar.svelte | 26 +++-- web/src/lib/services/photoprism.ts | 102 +++++++++++++++++- web/src/lib/stores/filters.svelte.ts | 53 +++++++++ web/src/routes/+page.svelte | 102 ++++++++++++++++-- web/src/routes/map/+page.svelte | 31 ++++++ 7 files changed, 363 insertions(+), 55 deletions(-) diff --git a/web/src/lib/components/layout/LeftSidebar.svelte b/web/src/lib/components/layout/LeftSidebar.svelte index 092a789..822cda3 100644 --- a/web/src/lib/components/layout/LeftSidebar.svelte +++ b/web/src/lib/components/layout/LeftSidebar.svelte @@ -41,6 +41,7 @@ } from '$lib/services/adapters/review'; import { filters, + navigateToFolder, setFolderPath, setSection, TAG_CATEGORIES, @@ -223,23 +224,40 @@ })); const folderCounts = $derived(folderCountsQuery.data ?? {}); - // Root entry shows "the user's library" — for admins without a - // BasePath that's still the whole library, served cheaply from - // /api/v1/config's `count.all`. For any user with a non-empty - // BasePath the precomputed total is wrong (it's library-wide), so we - // ask the sidecar for a recursive count rooted at the user's - // BasePath — listFolderCounts maps `""` through toOriginalsPath, which - // resolves to the BasePath itself, and the sidecar fan-out recurses. + // Root entry shows "the user's library" using the same filter the + // timeline applies at folderPath=='/' — empty q, which PhotoPrism + // resolves to the visible listing (no archived / hidden / review). + // Earlier this used /config's `count.all`, but that aggregate + // includes those buckets and didn't match what the user can actually + // click "select all" on; the discrepancy was confusing + // (LeftSidebar said 357, the action bar said ~329). + // + // `scopedRootCountQuery` retains the sidecar fan-out for users with + // a BasePath — `listFolderCounts(['''])` resolves `''` through + // `toOriginalsPath` to the user's BasePath and recurses, so it picks + // up the same subset PhotoPrism would. Empty BasePath admins use the + // PhotoPrism count-via-X-Count path so both surfaces agree. const scopedRootCountQuery = createQuery>(() => ({ queryKey: ['photos', 'root-count', userBasePath()], queryFn: () => listFolderCounts(['']), enabled: isAuthenticated() && userBasePath() !== '', staleTime: 60_000 })); + const visibleRootCountQuery = createQuery(() => ({ + queryKey: ['photos', 'visible-root-count', userBasePath()], + // `merged: true` so the count matches the timeline's photo entries + // (one per logical photo) rather than its file-row total. Without + // it, sidecar/companion files inflate the badge — e.g. a HEIC + JPG + // pair counts twice — and "select all" in the timeline never + // reaches the badge's number. + queryFn: () => countPhotos(scoped(''), { merged: true }), + enabled: isAuthenticated() && userBasePath() === '' && isAdminUser, + staleTime: 60_000 + })); const rootCount = $derived( userBasePath() === '' ? isAdminUser - ? (configQuery.data?.count?.all ?? 0) + ? (visibleRootCountQuery.data ?? 0) : 0 : (scopedRootCountQuery.data?.[''] ?? 0) ); @@ -490,14 +508,7 @@ } async function pickFolder(folderPath: string) { - // Folder selection works on top of the All Photos section; clearing - // the heap/section context mirrors mule-image's "drill into folder" - // behaviour. The URL sync $effect on the timeline picks this up. - setSection('all-photos'); - setFolderPath(folderPath); - const params = new URLSearchParams(); - params.set('folder', folderPath); - await goto(`/?${params.toString()}`, { keepFocus: true, noScroll: true }); + await navigateToFolder(folderPath); } function onCreateHeap() { diff --git a/web/src/lib/components/sidebar/RightSidebar.svelte b/web/src/lib/components/sidebar/RightSidebar.svelte index 23699d5..fa1a805 100644 --- a/web/src/lib/components/sidebar/RightSidebar.svelte +++ b/web/src/lib/components/sidebar/RightSidebar.svelte @@ -6,18 +6,20 @@ PUT (Details fields need the full body). -->