diff --git a/web/src/lib/components/layout/LeftSidebar.svelte b/web/src/lib/components/layout/LeftSidebar.svelte index d2a60ff..29e8bc8 100644 --- a/web/src/lib/components/layout/LeftSidebar.svelte +++ b/web/src/lib/components/layout/LeftSidebar.svelte @@ -13,25 +13,20 @@ deleteFolder, deleteHeap, duplicateHeap, - getAllMarks, getConfig, heapDownloadUrl, listFolderCounts, listFolders, - listGeo, listHeaps, logout, renameFolder, renameHeap, scanCrossFolderDuplicates, triggerDownload, - type AggregatedKeyword, type CrossFolderScanResult, - type PhotoMarksMap, type PpAlbum, type PpClientConfig, - type PpFolder, - type PpGeoCollection + type PpFolder } from '$lib/services/photoprism'; import { listDuplicateGroups, @@ -137,19 +132,12 @@ // (enabled:false via `wantScoped`) and the configQuery numbers are // used directly — same chrome as before that fix, no extra // round-trips. - const favoritesCountQuery = scopedCountQuery('favorites', 'favorite:true'); const reviewCountQuery = scopedCountQuery('review', 'review:true'); const hiddenCountQuery = scopedCountQuery('hidden', 'hidden:true'); const archivedCountQuery = scopedCountQuery('archived', 'archived:true'); - // Labels: PhotoPrism's q-DSL has no "has any label" predicate - // (`label:*` matches every photo, `label:` only matches the - // named label), so the sidebar uses `configQuery.count.labels` — the - // precomputed count of distinct label slugs. Same source the - // `tagCategoryCount('labels')` sub-row already uses, so the parent - // Tags badge sums the same numbers the sub-rows display. function bucketCount( - key: 'favorites' | 'review' | 'hidden' | 'archived', + key: 'review' | 'hidden' | 'archived', query: { data: number | undefined; isPending: boolean } ): number | undefined { if (wantScoped) { @@ -163,13 +151,6 @@ return c[key]; } - const marksQuery = createQuery(() => ({ - queryKey: ['marks'], - queryFn: getAllMarks, - enabled: isAuthenticated(), - staleTime: 60_000 - })); - // Duplicates counts for the sidebar badge. Stacks is a cheap // PhotoPrism query so we always fetch it; cross-folder is an // O(disk) scan, so the sidebar only *observes* its cache @@ -189,50 +170,6 @@ staleTime: 5 * 60_000 })); - // Geotagged-photo count for the Map sidebar badge. PhotoPrism's - // `count.places` is the number of distinct *locations* (cities/states), - // not the number of geotagged photos — so the sidebar would disagree - // with the "N geotagged" footer on /map. Sharing the `['geo']` cache - // keeps both numbers in lockstep and is free after /map's first visit. - const geoQuery = createQuery(() => ({ - queryKey: ['geo'], - queryFn: () => listGeo(), - enabled: isAuthenticated(), - staleTime: 5 * 60_000 - })); - - // Keywords contribution to the Tags badge. Aggregation is heavy - // (1000-photo fan-out), so the sidebar observes the cache populated - // by /tags?tab=keywords rather than triggering its own fetch — same - // lazy pattern as the cross-folder duplicates count above. - const keywordsQuery = createQuery(() => ({ - queryKey: ['photos', 'keywords'], - queryFn: aggregateKeywords, - enabled: false, - staleTime: 5 * 60_000 - })); - - const ratingsCount = $derived(countRatings(marksQuery.data)); - const colorsCount = $derived(countColors(marksQuery.data)); - - function countRatings(marks: PhotoMarksMap | undefined): number { - if (!marks) return 0; - let n = 0; - for (const m of Object.values(marks)) { - if ((m.rating ?? 0) > 0) n++; - } - return n; - } - - function countColors(marks: PhotoMarksMap | undefined): number { - if (!marks) return 0; - let n = 0; - for (const m of Object.values(marks)) { - if (m.color) n++; - } - return n; - } - const folderTree = $derived( buildTree((foldersQuery.data ?? []).map((f) => f.Path)) ); @@ -290,16 +227,12 @@ : (scopedRootCountQuery.data?.[''] ?? 0) ); - // Favorites / Review / Hidden / Archive nav entries use these - // derived values rather than peeking at configQuery directly so the - // scoped path is invisible to the views[]/manageViews[] declarations. - const favoritesBadge = $derived(bucketCount('favorites', favoritesCountQuery)); + // Review / Hidden / Archive nav entries use these derived values + // rather than peeking at configQuery directly so the scoped path + // is invisible to the manageViews[] declarations. const reviewBadge = $derived(bucketCount('review', reviewCountQuery)); const hiddenBadge = $derived(bucketCount('hidden', hiddenCountQuery)); const archivedBadge = $derived(bucketCount('archived', archivedCountQuery)); - const labelsBadge = $derived( - configQuery.data?.count?.labels - ); const createMut = createMutation(() => ({ mutationFn: (title: string) => createHeap(title), @@ -391,24 +324,6 @@ ratings: 'Ratings' }; - function tagCategoryCount(cat: TagCategory): number | undefined { - // Labels reads PhotoPrism's pre-computed distinct-label counter - // (`/api/v1/config` → count.labels), not the photo-count from - // `countPhotos('label:*')`. The photo-count returned 0 on libraries - // whose indexer hadn't surfaced labelled photos yet, leaving the - // badge silently empty; the precomputed counter is always present - // and reads as "how many labels you can pick from", matching the - // Keywords sub-row's distinct-count semantics. - if (cat === 'labels') return configQuery.data?.count?.labels; - if (cat === 'keywords') return keywordsQuery.data?.length; - // People follows the same distinct-count semantics as Labels — - // `/api/v1/config.count.people` is the number of named subjects PP - // has clustered, surfaced eagerly without a separate /subjects fetch. - if (cat === 'people') return configQuery.data?.count?.people; - if (cat === 'ratings') return ratingsCount; - return colorsCount; - } - function isTagCategoryActive(cat: TagCategory): boolean { return page.url.pathname.startsWith(`/tags/${cat}`); } @@ -554,14 +469,12 @@ // // `getCount` is a getter (not a snapshot) so the badge reads the latest // derived value on every render — the arrays themselves are constant. - // `count.all` already excludes archived/review/hidden (PhotoPrism's - // "everything visible in the main timeline" tally), so it matches what - // the All photos view actually renders. Map uses the shared `['geo']` - // cache so its badge matches /map's "N geotagged" footer exactly — - // `count.places` would have shown distinct locations instead. - // Review rolls in the duplicates tabs hosted under /review — stacks - // always contributes; cross-folder only contributes once its tab has - // been opened (the scan is lazy, not eager from the sidebar). + // Map and Tags intentionally render without a count badge; the count + // columns inside the TagsBrowserSidebar are the canonical surface for + // per-tag totals. Review rolls in the duplicates tabs hosted under + // /review — stacks always contributes; cross-folder only contributes + // once its tab has been opened (the scan is lazy, not eager from the + // sidebar). type ViewItem = | { kind: 'section'; id: Section; label: string; getCount: () => number | undefined } | { kind: 'route'; href: string; label: string; getCount: () => number | undefined }; @@ -571,28 +484,13 @@ // separate "everything regardless of folder" destination would just // duplicate it for users whose photos live under the root. const views: ViewItem[] = [ - // Map's `geoQuery` already returns the GeoJSON the user is - // permitted to see (PhotoPrism's /geo applies the session ACL), - // so the badge is per-user-correct without extra scoping. - { kind: 'route', href: '/map', label: 'Map', getCount: () => geoQuery.data?.features?.length } + { kind: 'route', href: '/map', label: 'Map', getCount: () => undefined } // Tags is rendered as a bespoke expandable block below the // `views` loop — it has sub-categories (Labels/Keywords/Colors/ // Ratings) and a chevron, neither of which fits the flat // section/route ViewItem shape. ]; - // Total badge for the "Tags" header row. Sum of the same per-category - // counts the sub-rows render (labels/people: distinct slugs/subjects, - // keywords: distinct keywords, ratings/colors: photos carrying each - // mark). Library-wide numbers — the badge is a quick "how many tags - // exist?" rollup, not a per-user-visible count. - const tagsTotal = $derived.by(() => { - if (labelsBadge === undefined) return undefined; - const keywords = keywordsQuery.data?.length ?? 0; - const people = configQuery.data?.count?.people ?? 0; - return labelsBadge + keywords + people + ratingsCount + colorsCount; - }); - const manageViews: ViewItem[] = [ { kind: 'route', @@ -900,9 +798,10 @@ {@render viewRow(v)} {/each} {#if tagsExpanded} {#each TAG_CATEGORIES as cat (cat)} {@const active = isTagCategoryActive(cat)} - {@const count = tagCategoryCount(cat)} {TAG_CATEGORY_LABELS[cat]} - {#if count !== undefined} - - {count} - - {/if} {/each} {/if}