feat: Dedup enhancements (streaming, retry, filename mode, scroll, auto-select)
Iterazioni sul feature Dedup (v1.10.0) da subito-post-scaffolding: - compute_fingerprint espone il vero errore invece del generico None, worker propaga l'err_msg via progress_callback esteso - Retry automatico: -length 30 su "invalid data / decoding frame", -length 60 + -algorithm 1 su "fingerprint vuoto" - Nuovo metodo "filename" (Jaccard sui token nome file) con algoritmo incrementale O(N·K) — utile per file corrotti o per anteprima veloce - Streaming groups: sia fingerprint che filename emettono `dedup:group` appena un gruppo raggiunge >=2 file; JS accumula in Map per update in-place. Progress bar avanza in tempo reale - Fix shape bug che dava "NaN duplicati" nella summary line - .dedup-groups-scroll: max-height 55vh + overflow-y auto per non perdere l'header/footer scrollando molti gruppi - Bottone "Seleziona tutti i consigliati" nel footer: ripristina la selezione di default (tutti tranne il TIENI marcati per cancellazione) - .gitignore: dedup_cache.db (SQLite locale per macchina utente)
This commit is contained in:
1 parent
00ff5a6220
commit
e7d11d0235
8 files changed
+315
-47
No files matched your search
+51
-1
@@ -3011,6 +3011,7 @@ const DedupUI = (() => {
|
||||
mstate.recursive = $("#dedup-recursive").checked;
|
||||
mstate.scanning = true;
|
||||
mstate.groups = [];
|
||||
if (mstate._groupIndex) mstate._groupIndex.clear();
|
||||
mstate.toDelete.clear();
|
||||
renderGroups();
|
||||
$("#dedup-start-btn").disabled = true;
|
||||
@@ -3022,11 +3023,13 @@ const DedupUI = (() => {
|
||||
$("#dedupCounter").textContent = "Scansione in corso…";
|
||||
setStatus("Scansione in corso...", "loading");
|
||||
|
||||
const method = $("#dedup-method")?.value || "fingerprint";
|
||||
let res;
|
||||
try {
|
||||
res = await window.pywebview.api.dedup_start_scan({
|
||||
directory: mstate.folder,
|
||||
recursive: mstate.recursive,
|
||||
method,
|
||||
});
|
||||
} catch (e) {
|
||||
setStatus("Errore avvio: " + ((e && e.message) || e), "error");
|
||||
@@ -3099,6 +3102,22 @@ const DedupUI = (() => {
|
||||
$("#dedup-start-btn").addEventListener("click", startScan);
|
||||
$("#dedup-stop-btn").addEventListener("click", stopScan);
|
||||
$("#dedup-trash-btn").addEventListener("click", moveSelectedToTrash);
|
||||
$("#dedup-select-suggested-btn").addEventListener("click", () => {
|
||||
// Reset selezione: per ogni gruppo, marca-per-cancellazione TUTTI
|
||||
// tranne il primo (il "TIENI" consigliato). Non tocca i primi.
|
||||
mstate.toDelete.clear();
|
||||
mstate.groups.forEach((group) => {
|
||||
for (let i = 1; i < group.length; i++) {
|
||||
if (group[i] && group[i].path) mstate.toDelete.add(group[i].path);
|
||||
}
|
||||
});
|
||||
// Sync checkbox nel DOM
|
||||
$$("#dedup-groups-wrap .dedup-file-check").forEach((cb) => {
|
||||
if (cb.disabled) return; // skip il "TIENI"
|
||||
cb.checked = true;
|
||||
});
|
||||
updateSelectedCount();
|
||||
});
|
||||
$("#dedup-recursive").addEventListener("change", (e) => {
|
||||
mstate.recursive = e.target.checked;
|
||||
});
|
||||
@@ -3114,6 +3133,9 @@ const DedupUI = (() => {
|
||||
$("#dedup-recursive").checked = cfg.dedup_recursive;
|
||||
mstate.recursive = cfg.dedup_recursive;
|
||||
}
|
||||
if (cfg.dedup_method && $("#dedup-method")) {
|
||||
$("#dedup-method").value = cfg.dedup_method;
|
||||
}
|
||||
|
||||
// Bridge handlers
|
||||
bridgeHandlers["dedup:progress"] = (p) => {
|
||||
@@ -3136,8 +3158,36 @@ const DedupUI = (() => {
|
||||
}
|
||||
};
|
||||
|
||||
// Streaming: gruppi arrivano uno alla volta man mano che si formano.
|
||||
// mstate.groups è una lista di ARRAY di entries (shape richiesta da
|
||||
// renderGroups). Manteniamo un indice group_id → arrayRef per poter
|
||||
// aggiornare in-place quando il gruppo cresce.
|
||||
if (!mstate._groupIndex) mstate._groupIndex = new Map();
|
||||
bridgeHandlers["dedup:group"] = (p) => {
|
||||
if (!p || !p.id || !Array.isArray(p.entries)) return;
|
||||
const idx = mstate._groupIndex;
|
||||
const existing = idx.get(p.id);
|
||||
if (existing) {
|
||||
// Aggiorna elementi in-place (stesso riferimento array in mstate.groups)
|
||||
existing.length = 0;
|
||||
for (const e of p.entries) existing.push(e);
|
||||
} else {
|
||||
// Nuovo gruppo: array copia + tag `_id` non-enumerabile per il lookup
|
||||
const arr = p.entries.slice();
|
||||
Object.defineProperty(arr, "_id", { value: p.id, enumerable: false });
|
||||
idx.set(p.id, arr);
|
||||
mstate.groups.push(arr);
|
||||
}
|
||||
renderGroups();
|
||||
setStatus(`Trovati ${mstate.groups.length} gruppi finora…`, "loading");
|
||||
};
|
||||
|
||||
bridgeHandlers["dedup:done"] = (p) => {
|
||||
mstate.groups = (p && p.groups) || [];
|
||||
// Se sono arrivati group via streaming, ignoriamo p.groups (già in mstate).
|
||||
// Fallback: se lo streaming non ha popolato nulla, usiamo p.groups.
|
||||
if (mstate.groups.length === 0 && p && Array.isArray(p.groups)) {
|
||||
mstate.groups = p.groups;
|
||||
}
|
||||
renderGroups();
|
||||
if (p && p.ok === false) {
|
||||
setStatus(p.error || "Errore scansione", "error");
|
||||
|
||||
Reference in new issue
Block a user