Enable cross-search persistent selection, explicit disk deletion confirmations, and 1-click duplicate copy deletion

This commit is contained in:
drjones
2026-07-20 21:15:06 -07:00
parent d017297007
commit 82e93972c1
2 changed files with 41 additions and 11 deletions

View File

@@ -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 = `<img src="/api/thumbnail?path=${encodeURIComponent(rec.path)}">
<div class="name" title="${rec.path}">${escapeHtml(rec.name)}</div>
<button>Reveal in Finder</button>`;
<button class="btn-reveal" style="width:100%;margin-top:4px">Reveal in Finder</button>
<button class="btn-del-dupe btn-danger" style="width:100%;margin-top:4px">🗑 Delete copy</button>`;
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);

View File

@@ -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