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:
@@ -12,9 +12,10 @@
|
||||
mule-sidecar. Resolution moves the unwanted copies into a
|
||||
`.duplicates/` quarantine folder PhotoPrism's indexer ignores.
|
||||
|
||||
The cross-folder scan is opt-in (button-triggered) rather than
|
||||
auto-run because it's an O(disk) operation. With size pre-filtering
|
||||
the scan stays fast (~250ms for 400 files in practice).
|
||||
The cross-folder scan auto-fires when its tab is active — with size
|
||||
pre-filtering it stays fast (~250ms for 400 files in practice) and a
|
||||
long staleTime keeps tab bounces from re-running it. The button is
|
||||
kept for manual "rescan after I moved files" refreshes.
|
||||
|
||||
Tabs themselves render in the parent route's Toolbar so they line up
|
||||
visually with the `/tags` pill row.
|
||||
@@ -42,23 +43,19 @@
|
||||
|
||||
const qc = useQueryClient();
|
||||
|
||||
// Cross-folder scan is a manually-triggered query: `enabled` stays
|
||||
// false until the user clicks "Scan filesystem". Subsequent clicks
|
||||
// invalidate the cache so each press kicks a fresh scan.
|
||||
let scanRequested = $state(false);
|
||||
// Cross-folder scan auto-fires when the tab is active. The 5-minute
|
||||
// staleTime means a fresh visit reuses the prior result; the
|
||||
// "Rescan filesystem" button invalidates to force a re-scan after
|
||||
// the user has moved files around.
|
||||
const crossQuery = createQuery<CrossFolderScanResult>(() => ({
|
||||
queryKey: ['duplicates-cross-folder'],
|
||||
queryFn: scanCrossFolderDuplicates,
|
||||
enabled: scanRequested && activeTab === 'cross-folder',
|
||||
enabled: activeTab === 'cross-folder',
|
||||
staleTime: 5 * 60_000
|
||||
}));
|
||||
|
||||
function triggerScan() {
|
||||
if (scanRequested && !crossQuery.isFetching) {
|
||||
void qc.invalidateQueries({ queryKey: ['duplicates-cross-folder'] });
|
||||
} else {
|
||||
scanRequested = true;
|
||||
}
|
||||
function rescan() {
|
||||
void qc.invalidateQueries({ queryKey: ['duplicates-cross-folder'] });
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
@@ -114,24 +111,17 @@
|
||||
type="button"
|
||||
class="inline-flex shrink-0 items-center gap-1.5 rounded-md border border-border px-3 py-1.5 text-xs hover:bg-accent disabled:opacity-50"
|
||||
disabled={crossQuery.isFetching}
|
||||
onclick={triggerScan}
|
||||
onclick={rescan}
|
||||
>
|
||||
{#if crossQuery.isFetching}
|
||||
Scanning…
|
||||
{:else if scanRequested}
|
||||
Rescan filesystem
|
||||
{:else}
|
||||
Scan filesystem
|
||||
Rescan filesystem
|
||||
{/if}
|
||||
</button>
|
||||
</header>
|
||||
|
||||
{#if !scanRequested}
|
||||
<p class="text-sm text-muted-foreground">
|
||||
Click <em>Scan filesystem</em> to look for byte-identical files spread across
|
||||
folders. Pre-filtered by size, so even large libraries finish in a few seconds.
|
||||
</p>
|
||||
{:else if crossQuery.isFetching && !crossQuery.data}
|
||||
{#if crossQuery.isFetching && !crossQuery.data}
|
||||
<p class="text-sm text-muted-foreground">Hashing files under originals…</p>
|
||||
{:else if crossQuery.isError}
|
||||
<p class="text-sm text-destructive">
|
||||
|
||||
Reference in New Issue
Block a user