From 281358ff046e8d1447da37e500d6004a94890ec5 Mon Sep 17 00:00:00 2001 From: Masahiko AMANO Date: Mon, 22 Jun 2026 22:26:08 +0300 Subject: [PATCH] feat(frontend): open cluster files in the full viewer for comparison MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The lightbox only showed the preview image, but dedup decisions need the metadata — date, tags, EXIF — to choose a survivor. Replace it with the same FileViewer the files page uses, mounted as a full-screen overlay with prev/ next wired across the cluster's files. A review toggle made in the viewer is mirrored back into the cluster list. Drops the now-superseded PreviewLightbox. --- .../components/file/PreviewLightbox.svelte | 252 ------------------ .../src/routes/files/duplicates/+page.svelte | 74 +++-- 2 files changed, 59 insertions(+), 267 deletions(-) delete mode 100644 frontend/src/lib/components/file/PreviewLightbox.svelte diff --git a/frontend/src/lib/components/file/PreviewLightbox.svelte b/frontend/src/lib/components/file/PreviewLightbox.svelte deleted file mode 100644 index fc0a526..0000000 --- a/frontend/src/lib/components/file/PreviewLightbox.svelte +++ /dev/null @@ -1,252 +0,0 @@ - - - - - - - - - diff --git a/frontend/src/routes/files/duplicates/+page.svelte b/frontend/src/routes/files/duplicates/+page.svelte index 0e91fd4..3b6eeed 100644 --- a/frontend/src/routes/files/duplicates/+page.svelte +++ b/frontend/src/routes/files/duplicates/+page.svelte @@ -4,7 +4,7 @@ import { getDuplicates, dismissDuplicate } from '$lib/api/duplicates'; import Thumb from '$lib/components/file/Thumb.svelte'; import DuplicateMergeDialog from '$lib/components/file/DuplicateMergeDialog.svelte'; - import PreviewLightbox from '$lib/components/file/PreviewLightbox.svelte'; + import FileViewer from '$lib/components/file/FileViewer.svelte'; import type { File } from '$lib/api/types'; const LIMIT = 20; @@ -35,9 +35,10 @@ let mergeKeep = $state(null); let mergeDiscard = $state(null); - // Enlarged-preview lightbox: thumbnails are too small to tell near-duplicates - // apart, so a zoom opens the full preview and pages across the cluster. - let lightbox = $state<{ files: File[]; startId: string } | null>(null); + // Full viewer (same as the files page): thumbnails are too small to compare, + // and dedup decisions need date / tags / EXIF, so the zoom opens the real + // viewer and pages across the cluster's files. + let viewer = $state<{ key: number; files: File[]; id: string } | null>(null); $effect(() => { if (!initialLoaded && !loading) void load(); @@ -103,8 +104,36 @@ } } - function openLightbox(c: Cluster, startId: string) { - lightbox = { files: c.files, startId }; + function openViewer(c: Cluster, id: string) { + viewer = { key: c.key, files: c.files, id }; + } + + // Prev/next within the cluster currently open in the viewer. + let viewerPrevId = $derived.by(() => { + const v = viewer; + if (!v) return null; + const i = v.files.findIndex((f) => f.id === v.id); + return i > 0 ? (v.files[i - 1]?.id ?? null) : null; + }); + let viewerNextId = $derived.by(() => { + const v = viewer; + if (!v) return null; + const i = v.files.findIndex((f) => f.id === v.id); + return i >= 0 && i < v.files.length - 1 ? (v.files[i + 1]?.id ?? null) : null; + }); + + function viewerNavigate(id: string) { + if (viewer) viewer = { ...viewer, id }; + } + + // Mirror a review toggle made inside the viewer back into the cluster list and + // the viewer's own navigation snapshot so both stay consistent. + function onViewerReviewChange(id: string, needsReview: boolean) { + const apply = (f: File) => (f.id === id ? { ...f, needs_review: needsReview } : f); + clusters = clusters.map((c) => + c.key === viewer?.key ? { ...c, files: c.files.map(apply) } : c + ); + if (viewer) viewer = { ...viewer, files: viewer.files.map(apply) }; } function openMerge(c: Cluster, other: File) { @@ -216,10 +245,10 @@ class="zoom" onclick={(e) => { e.stopPropagation(); - openLightbox(c, f.id); + openViewer(c, f.id); }} - aria-label="Enlarge preview" - title="Enlarge preview" + aria-label="Open in viewer" + title="Open in viewer" >
+ (viewer = null)} + onReviewChange={onViewerReviewChange} + /> +
{/if} {#if mergeKeep && mergeDiscard} @@ -493,4 +527,14 @@ color: var(--color-text-muted); font-size: 0.9rem; } + + /* Full-screen overlay for the file viewer, mirroring the files page. */ + .viewer-overlay { + position: fixed; + inset: 0; + z-index: 200; + background-color: var(--color-bg-primary); + overflow-y: auto; + overscroll-behavior: contain; + }