From a38c3c6e9be965e85c6403be030c882b99da8a0e Mon Sep 17 00:00:00 2001 From: dtoro Date: Mon, 18 May 2026 21:14:51 +0200 Subject: [PATCH] feat(duplicates): show count badges on Stacks/Cross-folder tabs Stacks count appears immediately (cheap query); cross-folder count fills in after its tab is visited (lazy disk scan). Page observes both queries from cache so badges stay in sync with the panel content. Co-Authored-By: Claude Opus 4.7 (1M context) --- web/src/routes/duplicates/+page.svelte | 47 +++++++++++++++++++++----- 1 file changed, 39 insertions(+), 8 deletions(-) diff --git a/web/src/routes/duplicates/+page.svelte b/web/src/routes/duplicates/+page.svelte index bb9303f..69e1dc6 100644 --- a/web/src/routes/duplicates/+page.svelte +++ b/web/src/routes/duplicates/+page.svelte @@ -6,6 +6,10 @@ listDuplicateGroups, type DuplicateGroup } from '$lib/services/adapters/duplicates'; + import { + scanCrossFolderDuplicates, + type CrossFolderScanResult + } from '$lib/services/photoprism'; import { isAuthenticated } from '$lib/stores/session.svelte'; import { setThumbnailSize, @@ -19,10 +23,6 @@ // Same pill-tab pattern as /tags: tab state is URL-driven so the user // can share / refresh / hit Back and land on the right panel. type Tab = 'stacks' | 'cross-folder'; - const TABS: { id: Tab; label: string }[] = [ - { id: 'stacks', label: 'Stacks' }, - { id: 'cross-folder', label: 'Cross-folder' } - ]; const activeTab = $derived(parseTab(page.url.searchParams.get('tab'))); function parseTab(raw: string | null): Tab { return raw === 'cross-folder' ? 'cross-folder' : 'stacks'; @@ -39,12 +39,31 @@ // Stale-time matches mule-image's DuplicatesView (30 s) so quick // toolbar bounces don't refetch the (potentially expensive) stack // listing. Invalidation by mutations is explicit, not time-driven. + // Enabled on both tabs so the stacks-count badge stays accurate even + // while the cross-folder tab is open. const dupesQuery = createQuery(() => ({ queryKey: ['duplicates'], queryFn: listDuplicateGroups, - enabled: isAuthenticated() && activeTab === 'stacks', + enabled: isAuthenticated(), staleTime: 30_000 })); + + // Observe-only: DuplicatesView's matching query (same key) is what + // actually triggers the scan when the cross-folder tab is active. + // Here we just read the cached count for the tab badge. + const crossQuery = createQuery(() => ({ + queryKey: ['duplicates-cross-folder'], + queryFn: scanCrossFolderDuplicates, + enabled: false, + staleTime: 5 * 60_000 + })); + + const stacksCount = $derived(dupesQuery.data?.length); + const crossCount = $derived(crossQuery.data?.groups.length); + const TABS = $derived<{ id: Tab; label: string; count: number | undefined }[]>([ + { id: 'stacks', label: 'Stacks', count: stacksCount }, + { id: 'cross-folder', label: 'Cross-folder', count: crossCount } + ]); @@ -52,17 +71,29 @@ Duplicates + treatment, same hover affordance. Count badge appears once the + underlying query has data — cross-folder stays unbadged until + the tab has been opened at least once (lazy scan). -->
{#each TABS as t (t.id)} {/each}