From 82e93972c177b8aecf99f7e7c7126a5be66f62a3 Mon Sep 17 00:00:00 2001 From: drjones Date: Mon, 20 Jul 2026 21:15:06 -0700 Subject: [PATCH] Enable cross-search persistent selection, explicit disk deletion confirmations, and 1-click duplicate copy deletion --- index.html | 23 +++++++++++++++++------ server.py | 29 ++++++++++++++++++++++++----- 2 files changed, 41 insertions(+), 11 deletions(-) diff --git a/index.html b/index.html index 458ff26..38763d8 100644 --- a/index.html +++ b/index.html @@ -1549,15 +1549,15 @@ let lastClickedIndex = null; $("btnSelectMode").onclick = ()=>{ selectMode = !selectMode; - $("btnSelectMode").textContent = selectMode ? "☑ Exit Select" : "☐ Select"; - if(!selectMode){ selectedPaths.clear(); lastClickedIndex = null; } + $("btnSelectMode").textContent = selectMode ? "☑ Exit Select Mode" : (selectedPaths.size ? `☑ Select (${selectedPaths.size})` : "☐ Select"); renderSearchResults(); updateBulkBar(); }; function updateBulkBar(){ - $("bulkBar").style.display = selectMode ? "flex" : "none"; - $("bulkCount").textContent = selectedPaths.size + " selected"; + const hasSelected = selectedPaths.size > 0; + $("bulkBar").style.display = (selectMode || hasSelected) ? "flex" : "none"; + $("bulkCount").textContent = selectedPaths.size + " selected (across searches)"; $("btnSelectAllMatching").textContent = "Select all " + (searchTotal || 0) + " matching"; } @@ -1796,9 +1796,20 @@ async function loadDupeGroups(){ t.className = "dupe-thumb"; t.innerHTML = `
${escapeHtml(rec.name)}
- `; + + `; t.querySelector("img").onclick = ()=>openQuickView(rec); - t.querySelector("button").onclick = ()=>api("/api/reveal", {path: rec.path}); + t.querySelector(".btn-reveal").onclick = ()=>api("/api/reveal", {path: rec.path}); + t.querySelector(".btn-del-dupe").onclick = async ()=>{ + if(!confirm(`PERMANENT DISK DELETION\n\nAre you sure you want to permanently delete this duplicate file from disk?\n\nFile: ${rec.path}\n\nThis cannot be undone.`)) return; + const resp = await api("/api/delete_photo", {path: rec.path}); + if(resp.ok){ + showToast("Deleted duplicate copy: " + os_basename(rec.path), "ok"); + loadDupeGroups(); + } else { + showToast(resp.error || "Failed to delete duplicate file", "error"); + } + }; rowEl.appendChild(t); }); grp.appendChild(rowEl); diff --git a/server.py b/server.py index b4b9d6a..d06cc07 100644 --- a/server.py +++ b/server.py @@ -523,14 +523,33 @@ def build_dedupe_index(): S.dedupe_status = "idle" def find_duplicate_groups(): - """Groups of exact perceptual-hash matches — visually identical / re-saved copies.""" + """Groups of exact perceptual-hash matches and size/filename matches.""" records = {r["path"]: r for r in load_search_index()} cache = load_phash_cache() - by_hash = {} + groups_by_key = {} + + # 1. Group by pHash if available for path, h in cache.items(): - if path in records: - by_hash.setdefault(h, []).append(records[path]) - groups = [g for g in by_hash.values() if len(g) > 1] + if path in records and os.path.exists(path): + groups_by_key.setdefault(f"phash:{h}", []).append(records[path]) + + # 2. Also group by (file size, filename) for non-hashed or exact size matches + by_size_name = {} + for path, r in records.items(): + if path not in cache and os.path.exists(path): + try: + sz = os.path.getsize(path) + name = os.path.basename(path).lower() + key = f"size:{sz}_{name}" + by_size_name.setdefault(key, []).append(r) + except Exception: + pass + + for key, g in by_size_name.items(): + if len(g) > 1: + groups_by_key[key] = g + + groups = [g for g in groups_by_key.values() if len(g) > 1] groups.sort(key=len, reverse=True) return groups