feat(duplicates): sidebar count + auto-run cross-folder scan
Sidebar Duplicates badge now sums stacks + cross-folder groups, with cross-folder observed from cache (no eager disk scan from the sidebar). Cross-folder tab auto-fires the scan on access; button becomes Rescan. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -21,13 +21,19 @@
|
||||
logout,
|
||||
renameFolder,
|
||||
renameHeap,
|
||||
scanCrossFolderDuplicates,
|
||||
triggerDownload,
|
||||
type CrossFolderScanResult,
|
||||
type ImportInfo,
|
||||
type PhotoMarksMap,
|
||||
type PpAlbum,
|
||||
type PpClientConfig,
|
||||
type PpFolder
|
||||
} from '$lib/services/photoprism';
|
||||
import {
|
||||
listDuplicateGroups,
|
||||
type DuplicateGroup
|
||||
} from '$lib/services/adapters/duplicates';
|
||||
import {
|
||||
filters,
|
||||
setFolderPath,
|
||||
@@ -101,6 +107,25 @@
|
||||
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
|
||||
// (enabled:false) and the duplicates page itself is what populates
|
||||
// it on first visit. Both share queryKeys with the /duplicates
|
||||
// view so cache is reused.
|
||||
const stacksQuery = createQuery<DuplicateGroup[]>(() => ({
|
||||
queryKey: ['duplicates'],
|
||||
queryFn: listDuplicateGroups,
|
||||
enabled: isAuthenticated(),
|
||||
staleTime: 60_000
|
||||
}));
|
||||
const crossFolderQuery = createQuery<CrossFolderScanResult>(() => ({
|
||||
queryKey: ['duplicates-cross-folder'],
|
||||
queryFn: scanCrossFolderDuplicates,
|
||||
enabled: false,
|
||||
staleTime: 5 * 60_000
|
||||
}));
|
||||
|
||||
const ratingsCount = $derived(countRatings(marksQuery.data));
|
||||
const colorsCount = $derived(countColors(marksQuery.data));
|
||||
|
||||
@@ -365,7 +390,9 @@
|
||||
// "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.
|
||||
// Duplicates has no precomputed counter; we omit its badge.
|
||||
// Duplicates sums stacks + cross-folder groups; cross-folder only
|
||||
// contributes once its tab has been visited (the scan is opt-in
|
||||
// per-visit, 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 };
|
||||
@@ -395,7 +422,16 @@
|
||||
|
||||
const manageViews: ViewItem[] = [
|
||||
{ kind: 'route', href: '/review', label: 'Review', getCount: () => configQuery.data?.count?.review },
|
||||
{ kind: 'route', href: '/duplicates', label: 'Duplicates', getCount: () => undefined },
|
||||
{
|
||||
kind: 'route',
|
||||
href: '/duplicates',
|
||||
label: 'Duplicates',
|
||||
getCount: () => {
|
||||
const stacks = stacksQuery.data?.length;
|
||||
if (stacks === undefined) return undefined;
|
||||
return stacks + (crossFolderQuery.data?.groups.length ?? 0);
|
||||
}
|
||||
},
|
||||
{ kind: 'section', id: 'hidden', label: 'Hidden', getCount: () => configQuery.data?.count?.hidden },
|
||||
{ kind: 'section', id: 'archive', label: 'Archive', getCount: () => configQuery.data?.count?.archived }
|
||||
];
|
||||
|
||||
Reference in New Issue
Block a user