diff --git a/src/components/DuplicateFinder.tsx b/src/components/DuplicateFinder.tsx index 1ab85fe..7824a49 100644 --- a/src/components/DuplicateFinder.tsx +++ b/src/components/DuplicateFinder.tsx @@ -1,4 +1,5 @@ -import { useState } from "react"; +import { useRef, useState } from "react"; +import { useVirtualizer } from "@tanstack/react-virtual"; import { convertFileSrc } from "@tauri-apps/api/core"; import { DuplicateGroup, useGalleryStore } from "../store"; import { FolderScopeDropdown } from "./FolderScopeDropdown"; @@ -130,6 +131,17 @@ export function DuplicateFinder() { const [deleting, setDeleting] = useState(false); const [deleteResult, setDeleteResult] = useState(null); + // Virtualize the group list so a large result set (e.g. thousands of pairs) + // only mounts the on-screen cards. Group cards vary in height (number of + // copies wraps across rows), so heights are measured dynamically. + const scrollRef = useRef(null); + const virtualizer = useVirtualizer({ + count: duplicateGroups.length, + getScrollElement: () => scrollRef.current, + estimateSize: () => 220, + overscan: 4, + }); + const selectedCount = duplicateSelectedIds.size; const hasResults = duplicateGroups.length > 0; const hasScanned = hasResults || duplicateLastScanned !== null || (!duplicateScanning && duplicateScanProgress !== null); @@ -277,11 +289,29 @@ export function DuplicateFinder() {

No duplicate files found.

) : ( -
-
- {duplicateGroups.map((group) => ( - - ))} +
+
+ {virtualizer.getVirtualItems().map((virtualItem) => { + const group = duplicateGroups[virtualItem.index]; + if (!group) return null; + return ( +
+ +
+ ); + })}
)}