From c5f407a88b61c7347ead84cce327d6da142e394f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 19 May 2026 08:42:22 +0000 Subject: [PATCH 1/3] Initial plan From 24680e24a4ca94134b63336778e8921847041ea1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 19 May 2026 08:49:50 +0000 Subject: [PATCH 2/3] fix: add duplicate results pagination and full select Agent-Logs-Url: https://github.com/igeekfan/ft/sessions/45648c61-2d58-41bb-8d09-f1ae0aeab130 Co-authored-by: luoyunchong <18613266+luoyunchong@users.noreply.github.com> --- frontend/src/App.tsx | 51 ++++++++++++++++++++------- frontend/src/components/ActionBar.tsx | 10 +++++- frontend/src/components/Results.tsx | 45 ++++++++++++++++++++++- frontend/src/i18n/en-US.ts | 2 ++ frontend/src/i18n/zh-CN.ts | 2 ++ store.go | 11 +++++- 6 files changed, 105 insertions(+), 16 deletions(-) diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index dbcd382..640c282 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -448,20 +448,38 @@ function App() { const handleDeselectAll = () => setSelectedPaths(new Set()) - const handleSelectAll = useCallback(() => { + const handleSelectAll = useCallback(async () => { if (groups.length === 0) return - setSelectedPaths(prev => { - const next = new Set(prev) - groups.forEach(group => { + setLoading(true) + try { + const next = new Set(selectedPaths) + const pagesToFetch: number[] = [] + for (let page = 1; page <= pageInfo.totalPages; page++) { + if (page !== pageInfo.page) { + pagesToFetch.push(page) + } + } + + const currentGroups = groups + const otherPages = await Promise.all( + pagesToFetch.map(page => + GetGroupsPage(page, pageInfo.pageSize, sortBy, searchQuery) as Promise + ) + ) + const allGroups = [...currentGroups, ...otherPages.flatMap(pageData => pageData.groups || [])] + + allGroups.forEach(group => { group.files.forEach(file => { - if (!next.has(file.path)) { - next.add(file.path) - } + next.add(file.path) }) }) - return next - }) - }, [groups]) + setSelectedPaths(next) + } catch (err) { + console.error('SelectAll error:', err) + } finally { + setLoading(false) + } + }, [groups, pageInfo.page, pageInfo.pageSize, pageInfo.totalPages, searchQuery, selectedPaths, sortBy]) const handleDismissUpdate = () => { if (updateInfo) { @@ -521,7 +539,7 @@ function App() { // Ctrl+A - select all files if (e.ctrlKey && e.key === 'a') { e.preventDefault() - handleSelectAll() + void handleSelectAll() } // Escape - deselect all @@ -665,8 +683,14 @@ function App() { onToggleGroup={handleToggleGroup} onDeleteFile={handleDeleteFile} onPageChange={(page) => setPageInfo(prev => ({...prev, page}))} - onSortChange={setSortBy} - onSearchChange={setSearchQuery} + onSortChange={(value) => { + setSortBy(value) + setPageInfo(prev => ({...prev, page: 1})) + }} + onSearchChange={(value) => { + setSearchQuery(value) + setPageInfo(prev => ({...prev, page: 1})) + }} /> void handleSelectAll()} onSmartSelect={handleSmartSelect} onDelete={handleDelete} onDeselectAll={handleDeselectAll} diff --git a/frontend/src/components/ActionBar.tsx b/frontend/src/components/ActionBar.tsx index db63f7e..3616e63 100644 --- a/frontend/src/components/ActionBar.tsx +++ b/frontend/src/components/ActionBar.tsx @@ -1,6 +1,6 @@ import {DuplicateGroup} from '../types' import {Button} from '@/components/ui/button' -import {Trash2, X, FolderOpen, Sparkles} from 'lucide-react' +import {Trash2, X, FolderOpen, Sparkles, CheckCheck} from 'lucide-react' import {useI18n} from '../i18n/context' interface ActionBarProps { @@ -9,6 +9,7 @@ interface ActionBarProps { folders: string[] groups: DuplicateGroup[] onSelectFolderDuplicates: (folderPrefix: string) => void + onSelectAll: () => void onSmartSelect: () => void onDelete: () => void onDeselectAll: () => void @@ -32,6 +33,7 @@ function ActionBar({ folders, groups, onSelectFolderDuplicates, + onSelectAll, onSmartSelect, onDelete, onDeselectAll @@ -45,6 +47,12 @@ function ActionBar({ {t('actionBar.groupSummary', {count: groups.length, size: formatSize(totalWasted)})}
+ {groups.length > 0 && ( + + )} {groups.length > 0 && ( + + {t('results.pageStatus', {page: pageInfo.page, totalPages: pageInfo.totalPages, total: pageInfo.total})} + + { + const page = Number(e.target.value) + if (!Number.isFinite(page)) return + if (page < 1 || page > pageInfo.totalPages) return + onPageChange(page) + }} + /> + +
+ )} ) } diff --git a/frontend/src/i18n/en-US.ts b/frontend/src/i18n/en-US.ts index 4bf85e0..b176c2f 100644 --- a/frontend/src/i18n/en-US.ts +++ b/frontend/src/i18n/en-US.ts @@ -112,6 +112,7 @@ const enUS: typeof import('./zh-CN').default = { 'actionBar.groupSummary': '{count} duplicate groups, reclaimable {size}', 'actionBar.selectAll': 'Select All:', + 'actionBar.selectAllResults': 'Select All Results', 'actionBar.selected': '{count} files selected', 'actionBar.deselectAll': 'Deselect All', 'actionBar.deleteSelected': 'Delete Selected ({count})', @@ -151,6 +152,7 @@ const enUS: typeof import('./zh-CN').default = { 'results.openLocation': 'Open Location', 'results.openFile': 'Open File', 'results.deleteFile': 'Delete This File', + 'results.pageStatus': 'Page {page}/{totalPages} · {total} groups', 'actionBar.smartSelect': 'Smart Select', 'scan.groupingByFilename': 'Grouping results by filename...', diff --git a/frontend/src/i18n/zh-CN.ts b/frontend/src/i18n/zh-CN.ts index 28a6310..7693a06 100644 --- a/frontend/src/i18n/zh-CN.ts +++ b/frontend/src/i18n/zh-CN.ts @@ -112,6 +112,7 @@ const zhCN = { 'actionBar.groupSummary': '共 {count} 组重复,可释放 {size}', 'actionBar.selectAll': '全选:', + 'actionBar.selectAllResults': '全选查询结果', 'actionBar.selected': '已选中 {count} 个文件', 'actionBar.deselectAll': '取消全选', 'actionBar.deleteSelected': '删除选中 ({count})', @@ -151,6 +152,7 @@ const zhCN = { 'results.openLocation': '打开所在目录', 'results.openFile': '打开文件', 'results.deleteFile': '删除此文件', + 'results.pageStatus': '第 {page}/{totalPages} 页 · 共 {total} 组', 'actionBar.smartSelect': '智能选择', 'scan.groupingByFilename': '正在按文件名整理结果...', diff --git a/store.go b/store.go index 1fb9b9c..8205b44 100644 --- a/store.go +++ b/store.go @@ -268,7 +268,16 @@ func (s *Store) GetGroupsByScanID(scanID int64, page, pageSize int, sortBy, sear } var total int - s.db.QueryRow("SELECT COUNT(*) FROM groups WHERE scan_id = ?", scanID).Scan(&total) + if searchQuery != "" { + q := "%" + searchQuery + "%" + s.db.QueryRow( + `SELECT COUNT(*) FROM groups g + WHERE g.scan_id = ? AND EXISTS (SELECT 1 FROM files f WHERE f.group_id = g.id AND f.name LIKE ?)`, + scanID, q, + ).Scan(&total) + } else { + s.db.QueryRow("SELECT COUNT(*) FROM groups WHERE scan_id = ?", scanID).Scan(&total) + } totalPages := (total + pageSize - 1) / pageSize orderBy := "g.file_count * g.size DESC" From 1a2510b2d58218e1184714df7dd7a4c2cd06ea34 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 19 May 2026 08:58:20 +0000 Subject: [PATCH 3/3] fix: address review feedback for pagination select-all Agent-Logs-Url: https://github.com/igeekfan/ft/sessions/45648c61-2d58-41bb-8d09-f1ae0aeab130 Co-authored-by: luoyunchong <18613266+luoyunchong@users.noreply.github.com> --- frontend/src/App.tsx | 37 +++++++++++++++------------ frontend/src/components/ActionBar.tsx | 6 +++-- frontend/src/components/Results.tsx | 28 +++++++++++++++----- frontend/src/i18n/en-US.ts | 1 + frontend/src/i18n/zh-CN.ts | 1 + store.go | 20 ++++++++++++--- 6 files changed, 64 insertions(+), 29 deletions(-) diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 640c282..f5e1757 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -103,6 +103,7 @@ function App() { const [browserPath, setBrowserPath] = useState(() => localStorage.getItem(STORAGE_KEY_BROWSER_PATH) || '') const [toast, setToast] = useState<{message: string, type: 'success' | 'error'} | null>(null) const [loading, setLoading] = useState(false) + const [isSelectingAll, setIsSelectingAll] = useState(false) const [updateInfo, setUpdateInfo] = useState(null) const [showHistory, setShowHistory] = useState(false) const [showAnalysis, setShowAnalysis] = useState(false) @@ -449,37 +450,38 @@ function App() { const handleDeselectAll = () => setSelectedPaths(new Set()) const handleSelectAll = useCallback(async () => { - if (groups.length === 0) return - setLoading(true) + if (groups.length === 0 || isSelectingAll) return + setIsSelectingAll(true) try { - const next = new Set(selectedPaths) - const pagesToFetch: number[] = [] + const next = new Set() + const currentGroups = groups + const allGroups = [...currentGroups] for (let page = 1; page <= pageInfo.totalPages; page++) { - if (page !== pageInfo.page) { - pagesToFetch.push(page) + if (page === pageInfo.page) continue + try { + const pageData = await GetGroupsPage(page, pageInfo.pageSize, sortBy, searchQuery) as main.GroupPage + allGroups.push(...(pageData.groups || [])) + } catch (err) { + console.error(`SelectAll page ${page} error:`, err) } } - const currentGroups = groups - const otherPages = await Promise.all( - pagesToFetch.map(page => - GetGroupsPage(page, pageInfo.pageSize, sortBy, searchQuery) as Promise - ) - ) - const allGroups = [...currentGroups, ...otherPages.flatMap(pageData => pageData.groups || [])] - allGroups.forEach(group => { group.files.forEach(file => { next.add(file.path) }) }) - setSelectedPaths(next) + setSelectedPaths(prev => { + const merged = new Set(prev) + next.forEach(path => merged.add(path)) + return merged + }) } catch (err) { console.error('SelectAll error:', err) } finally { - setLoading(false) + setIsSelectingAll(false) } - }, [groups, pageInfo.page, pageInfo.pageSize, pageInfo.totalPages, searchQuery, selectedPaths, sortBy]) + }, [groups, isSelectingAll, pageInfo.page, pageInfo.pageSize, pageInfo.totalPages, searchQuery, sortBy]) const handleDismissUpdate = () => { if (updateInfo) { @@ -699,6 +701,7 @@ function App() { groups={groups} onSelectFolderDuplicates={handleSelectFolderDuplicates} onSelectAll={() => void handleSelectAll()} + selectingAll={isSelectingAll} onSmartSelect={handleSmartSelect} onDelete={handleDelete} onDeselectAll={handleDeselectAll} diff --git a/frontend/src/components/ActionBar.tsx b/frontend/src/components/ActionBar.tsx index 3616e63..ac640c2 100644 --- a/frontend/src/components/ActionBar.tsx +++ b/frontend/src/components/ActionBar.tsx @@ -10,6 +10,7 @@ interface ActionBarProps { groups: DuplicateGroup[] onSelectFolderDuplicates: (folderPrefix: string) => void onSelectAll: () => void + selectingAll: boolean onSmartSelect: () => void onDelete: () => void onDeselectAll: () => void @@ -34,6 +35,7 @@ function ActionBar({ groups, onSelectFolderDuplicates, onSelectAll, + selectingAll, onSmartSelect, onDelete, onDeselectAll @@ -48,9 +50,9 @@ function ActionBar({
{groups.length > 0 && ( - )} {groups.length > 0 && ( diff --git a/frontend/src/components/Results.tsx b/frontend/src/components/Results.tsx index 2125657..3f899ea 100644 --- a/frontend/src/components/Results.tsx +++ b/frontend/src/components/Results.tsx @@ -125,6 +125,7 @@ function Results({ const {t} = useI18n() const [activeFilter, setActiveFilter] = useState('all') const [searchInput, setSearchInput] = useState(searchQuery) + const [pageInput, setPageInput] = useState(String(pageInfo.page)) const [previewFile, setPreviewFile] = useState(null) const [imagePreviewUrls, setImagePreviewUrls] = useState>({}) const [brokenPreviewPaths, setBrokenPreviewPaths] = useState>({}) @@ -139,6 +140,19 @@ function Results({ setSearchInput(searchQuery) }, [searchQuery]) + useEffect(() => { + setPageInput(String(pageInfo.page)) + }, [pageInfo.page]) + + const applyPageInput = useCallback(() => { + const page = Number(pageInput) + if (Number.isFinite(page) && page >= 1 && page <= pageInfo.totalPages) { + onPageChange(page) + return + } + setPageInput(String(pageInfo.page)) + }, [onPageChange, pageInfo.page, pageInfo.totalPages, pageInput]) + const ensureImagePreview = useCallback(async (filePath: string) => { if (imagePreviewUrls[filePath] || brokenPreviewPaths[filePath] || loadingImagePaths[filePath]) { return @@ -501,13 +515,15 @@ function Results({ type="number" min={1} max={pageInfo.totalPages} + aria-label={t('results.jumpToPage')} className="h-7 w-16 rounded-md border bg-background px-2 text-xs" - value={pageInfo.page} - onChange={(e) => { - const page = Number(e.target.value) - if (!Number.isFinite(page)) return - if (page < 1 || page > pageInfo.totalPages) return - onPageChange(page) + value={pageInput} + onChange={(e) => setPageInput(e.target.value)} + onBlur={applyPageInput} + onKeyDown={(e) => { + if (e.key === 'Enter') { + applyPageInput() + } }} />