Enable cross-search persistent selection, explicit disk deletion confirmations, and 1-click duplicate copy deletion
This commit is contained in:
23
index.html
23
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 = `<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);
|
||||
|
||||
29
server.py
29
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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user