Replace the legacy mule-image backend with PhotoPrism plus a thin SvelteKit client and a Node sidecar for endpoints PhotoPrism doesn't expose (file rename), and add a two-phase migrator (metadata via PUT, heaps → albums) for the existing Postgres library. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
68 lines
2.2 KiB
Svelte
68 lines
2.2 KiB
Svelte
<script lang="ts">
|
|
import { createQuery } from '@tanstack/svelte-query';
|
|
import {
|
|
listDuplicateGroups,
|
|
type DuplicateGroup
|
|
} from '$lib/services/adapters/duplicates';
|
|
import { isAuthenticated } from '$lib/stores/session.svelte';
|
|
import {
|
|
setThumbnailSize,
|
|
THUMBNAIL_SIZE_LABELS,
|
|
THUMBNAIL_SIZE_PRESETS,
|
|
view
|
|
} from '$lib/stores/view.svelte';
|
|
import Toolbar from '$lib/components/layout/Toolbar.svelte';
|
|
import DuplicatesView from '$lib/components/duplicates/DuplicatesView.svelte';
|
|
|
|
// 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.
|
|
const dupesQuery = createQuery<DuplicateGroup[]>(() => ({
|
|
queryKey: ['duplicates'],
|
|
queryFn: listDuplicateGroups,
|
|
enabled: isAuthenticated(),
|
|
staleTime: 30_000
|
|
}));
|
|
</script>
|
|
|
|
<Toolbar>
|
|
<span class="rounded-md border border-border px-2 py-0.5 text-[11px] text-muted-foreground">
|
|
Duplicates · stacks
|
|
</span>
|
|
{#snippet trailing()}
|
|
<!-- Thumbnail-size control mirrors the timeline Toolbar. The view
|
|
store is global, so the picked size persists across routes —
|
|
when you come back to the timeline it stays where you left it. -->
|
|
<div
|
|
class="flex items-center overflow-hidden rounded border border-border"
|
|
role="group"
|
|
aria-label="Thumbnail size"
|
|
>
|
|
{#each THUMBNAIL_SIZE_PRESETS as size, i (size)}
|
|
<button
|
|
type="button"
|
|
class="px-1.5 py-0.5 text-[10px] font-medium hover:bg-accent"
|
|
class:bg-accent={view.thumbnailSize === size}
|
|
class:text-foreground={view.thumbnailSize === size}
|
|
class:text-muted-foreground={view.thumbnailSize !== size}
|
|
onclick={() => setThumbnailSize(size)}
|
|
title={`${THUMBNAIL_SIZE_LABELS[i]} · ${size}px`}
|
|
>
|
|
{THUMBNAIL_SIZE_LABELS[i]}
|
|
</button>
|
|
{/each}
|
|
</div>
|
|
<span class="text-[11px] text-muted-foreground">
|
|
{dupesQuery.data?.length ?? 0} group{dupesQuery.data?.length === 1 ? '' : 's'}
|
|
</span>
|
|
{/snippet}
|
|
</Toolbar>
|
|
|
|
<main class="min-h-0 flex-1 overflow-y-auto">
|
|
<DuplicatesView
|
|
groups={dupesQuery.data ?? []}
|
|
pending={dupesQuery.isPending}
|
|
error={dupesQuery.error}
|
|
/>
|
|
</main>
|