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) <noreply@anthropic.com>
This commit is contained in:
@@ -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<Tab>(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<DuplicateGroup[]>(() => ({
|
||||
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<CrossFolderScanResult>(() => ({
|
||||
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 }
|
||||
]);
|
||||
</script>
|
||||
|
||||
<Toolbar>
|
||||
@@ -52,17 +71,29 @@
|
||||
Duplicates
|
||||
</span>
|
||||
<!-- Tabs mirror /tags' pill row visually: same height, same active
|
||||
treatment, same hover affordance. -->
|
||||
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). -->
|
||||
<div class="flex items-center gap-1">
|
||||
{#each TABS as t (t.id)}
|
||||
<button
|
||||
type="button"
|
||||
class="rounded border px-2 py-0.5 text-[11px] {activeTab === t.id
|
||||
class="inline-flex items-center gap-1 rounded border px-2 py-0.5 text-[11px] {activeTab === t.id
|
||||
? 'border-primary/40 bg-primary/10 text-primary'
|
||||
: 'border-border text-muted-foreground hover:bg-accent hover:text-foreground'}"
|
||||
onclick={() => setTab(t.id)}
|
||||
>
|
||||
{t.label}
|
||||
<span>{t.label}</span>
|
||||
{#if t.count !== undefined}
|
||||
<span
|
||||
class="flex h-4 min-w-4.5 items-center justify-center rounded px-1 text-[10px] tabular-nums {activeTab ===
|
||||
t.id
|
||||
? 'bg-primary/15 text-primary'
|
||||
: 'bg-secondary text-muted-foreground'}"
|
||||
>
|
||||
{t.count}
|
||||
</span>
|
||||
{/if}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user