feat(sidebar,tags): per-tab counts and aggregated Tags badge
- /tags: each tab pill shows its own count (labels/keywords = distinct tags, ratings/colors = photos covered). Labels and marks queries become always-enabled on the route so every pill resolves immediately; keywords stays lazy. - LeftSidebar: Map badge now reads from the shared `['geo']` cache so it matches /map's "N geotagged" footer instead of count.places (distinct locations). Tags badge sums the four inner counts; keywords contributes lazily once /tags?tab=keywords is visited. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -6,6 +6,7 @@
|
||||
import { mode, toggleMode } from 'mode-watcher';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import {
|
||||
aggregateKeywords,
|
||||
createFolder,
|
||||
createHeap,
|
||||
deleteFolder,
|
||||
@@ -16,17 +17,20 @@
|
||||
heapDownloadUrl,
|
||||
listFolderCounts,
|
||||
listFolders,
|
||||
listGeo,
|
||||
listHeaps,
|
||||
logout,
|
||||
renameFolder,
|
||||
renameHeap,
|
||||
scanCrossFolderDuplicates,
|
||||
triggerDownload,
|
||||
type AggregatedKeyword,
|
||||
type CrossFolderScanResult,
|
||||
type PhotoMarksMap,
|
||||
type PpAlbum,
|
||||
type PpClientConfig,
|
||||
type PpFolder
|
||||
type PpFolder,
|
||||
type PpGeoCollection
|
||||
} from '$lib/services/photoprism';
|
||||
import {
|
||||
listDuplicateGroups,
|
||||
@@ -114,6 +118,29 @@
|
||||
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<PpGeoCollection>(() => ({
|
||||
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<AggregatedKeyword[]>(() => ({
|
||||
queryKey: ['photos', 'keywords'],
|
||||
queryFn: aggregateKeywords,
|
||||
enabled: false,
|
||||
staleTime: 5 * 60_000
|
||||
}));
|
||||
|
||||
const ratingsCount = $derived(countRatings(marksQuery.data));
|
||||
const colorsCount = $derived(countColors(marksQuery.data));
|
||||
|
||||
@@ -383,8 +410,9 @@
|
||||
// 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. `places` is the count of
|
||||
// geocoded locations — semantically what the Map view groups by.
|
||||
// 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).
|
||||
@@ -397,10 +425,11 @@
|
||||
// separate "everything regardless of folder" destination would just
|
||||
// duplicate it for users whose photos live under the root.
|
||||
const views: ViewItem[] = [
|
||||
{ kind: 'route', href: '/map', label: 'Map', getCount: () => configQuery.data?.count?.places },
|
||||
{ kind: 'route', href: '/map', label: 'Map', getCount: () => geoQuery.data?.features?.length },
|
||||
// Tags hosts four tabs (Labels (auto) / Keywords / Ratings / Colors);
|
||||
// the badge shows total labels + keywords + ratings + colors so the
|
||||
// number reflects the combined "things you can filter by" surface.
|
||||
// the badge sums each tab's badge so the sidebar number is the
|
||||
// total of what the inner tabs show. Keywords is lazy — it only
|
||||
// contributes after /tags?tab=keywords has been visited once.
|
||||
{
|
||||
kind: 'route',
|
||||
href: '/tags',
|
||||
@@ -408,7 +437,8 @@
|
||||
getCount: () => {
|
||||
const labels = configQuery.data?.count?.labels;
|
||||
if (labels === undefined) return undefined;
|
||||
return labels + ratingsCount + colorsCount;
|
||||
const keywords = keywordsQuery.data?.length ?? 0;
|
||||
return labels + keywords + ratingsCount + colorsCount;
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
Reference in New Issue
Block a user