ui: tab Cataloga con streaming table + modal conferma move
This commit is contained in:
1 parent
9b711fd545
commit
401bed967a
3 files changed
+698
No files matched your search
@@ -687,6 +687,7 @@ input[type="number"]::-webkit-inner-spin-button {
|
||||
.percent.green { color: var(--green-text); }
|
||||
.percent.blue { color: var(--blue-text); }
|
||||
.percent.pink { color: var(--pink-hover); }
|
||||
.percent.purple { color: var(--purple-text); }
|
||||
|
||||
.progress {
|
||||
width: 100%;
|
||||
@@ -715,6 +716,10 @@ input[type="number"]::-webkit-inner-spin-button {
|
||||
background: linear-gradient(90deg, var(--pink), var(--pink-hover));
|
||||
}
|
||||
|
||||
.progress-fill.purple {
|
||||
background: linear-gradient(90deg, var(--purple), var(--purple-hover));
|
||||
}
|
||||
|
||||
/* ===============================
|
||||
LOG BOX
|
||||
=============================== */
|
||||
@@ -1970,3 +1975,131 @@ input[type="number"]::-webkit-inner-spin-button {
|
||||
padding-right: 6px; /* spazio per la scrollbar sul bordo */
|
||||
}
|
||||
|
||||
/* ===============================
|
||||
CATALOGA tab
|
||||
=============================== */
|
||||
.catalog-summary {
|
||||
font-size: 14px;
|
||||
color: var(--text-2);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* Scroll container per la tabella (analogo a dedup-groups-scroll) */
|
||||
.catalog-scroll {
|
||||
max-height: 55vh;
|
||||
overflow-y: auto;
|
||||
overflow-x: auto;
|
||||
padding-right: 6px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--r-md);
|
||||
background: var(--bg-card);
|
||||
}
|
||||
|
||||
.catalog-table {
|
||||
width: 100%;
|
||||
border-collapse: separate;
|
||||
border-spacing: 0;
|
||||
font-size: 12px;
|
||||
color: var(--text-2);
|
||||
}
|
||||
|
||||
.catalog-table thead th {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 2;
|
||||
background: var(--bg-card-2);
|
||||
color: var(--text-1);
|
||||
font-weight: 700;
|
||||
padding: 10px 8px;
|
||||
text-align: left;
|
||||
font-size: 11px;
|
||||
letter-spacing: 0.3px;
|
||||
text-transform: uppercase;
|
||||
border-bottom: 1px solid var(--border);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.catalog-table tbody td {
|
||||
padding: 8px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
vertical-align: middle;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
max-width: 260px;
|
||||
}
|
||||
|
||||
.catalog-table tbody tr:hover {
|
||||
background: var(--bg-elev);
|
||||
}
|
||||
|
||||
/* Righe senza match AcoustID: sfondo rosa tenue */
|
||||
.catalog-table tbody tr.catalog-row-unmatched {
|
||||
background: rgba(236, 72, 153, 0.06);
|
||||
}
|
||||
.catalog-table tbody tr.catalog-row-unmatched:hover {
|
||||
background: rgba(236, 72, 153, 0.12);
|
||||
}
|
||||
|
||||
.catalog-col-check { width: 30px; text-align: center; }
|
||||
.catalog-col-year { width: 60px; text-align: center; font-variant-numeric: tabular-nums; }
|
||||
.catalog-col-status { width: 110px; }
|
||||
|
||||
.catalog-file-cell {
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
color: var(--text-1);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.catalog-check {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
cursor: pointer;
|
||||
accent-color: var(--purple);
|
||||
}
|
||||
|
||||
.catalog-status-badge {
|
||||
display: inline-block;
|
||||
padding: 2px 8px;
|
||||
border-radius: 10px;
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.3px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.catalog-status-badge.ok {
|
||||
background: var(--purple-dim);
|
||||
color: var(--purple-text);
|
||||
}
|
||||
.catalog-status-badge.warn {
|
||||
background: var(--amber-dim, rgba(245, 158, 11, 0.15));
|
||||
color: var(--amber-text);
|
||||
}
|
||||
.catalog-status-badge.err {
|
||||
background: rgba(220, 38, 38, 0.15);
|
||||
color: var(--red);
|
||||
}
|
||||
.catalog-status-badge.busy {
|
||||
background: var(--bg-input);
|
||||
color: var(--text-3);
|
||||
}
|
||||
|
||||
.catalog-footer-bar {
|
||||
position: sticky;
|
||||
bottom: 0;
|
||||
z-index: 5;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 12px 16px;
|
||||
margin-top: 12px;
|
||||
border-radius: var(--r-md);
|
||||
background: var(--bg-card);
|
||||
box-shadow: 0 -4px 12px rgba(0, 0, 0, 0.15);
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
|
||||
@@ -53,6 +53,10 @@
|
||||
<span class="nav-icon">🗑</span>
|
||||
<span>Dedup</span>
|
||||
</button>
|
||||
<button class="nav-item" data-view="catalog" data-feature="audio">
|
||||
<span class="nav-icon">📅</span>
|
||||
<span>Cataloga</span>
|
||||
</button>
|
||||
<button class="nav-item" data-view="upgrade" data-feature="upgrade">
|
||||
<span class="nav-icon">⚡</span>
|
||||
<span>Upgrade</span>
|
||||
@@ -1304,6 +1308,106 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ===== VIEW: CATALOGA (AcoustID -> <Anno>/<Genere>/) ===== -->
|
||||
<section class="view" id="view-catalog">
|
||||
<header class="hero hero-purple">
|
||||
<div class="hero-content">
|
||||
<div class="hero-eyebrow purple">CATALOGA</div>
|
||||
<h1 class="hero-title">Organizza per anno e genere</h1>
|
||||
<p class="hero-subtitle">Identifica ogni brano via audio fingerprint (Chromaprint + AcoustID) e sposta i file in <code><Anno>/<Genere>/</code>.</p>
|
||||
</div>
|
||||
<div class="hero-deco">📅</div>
|
||||
</header>
|
||||
|
||||
<h2 class="section-label">Sorgente</h2>
|
||||
<div class="card">
|
||||
<div class="beatport-header">
|
||||
<button id="catalog-pick-source" class="btn btn-primary pill">📁 Cartella input</button>
|
||||
<button id="catalog-start-btn" class="btn btn-primary pill" disabled>
|
||||
<span class="ico">▶</span> Scansiona
|
||||
</button>
|
||||
<button id="catalog-stop-btn" class="btn btn-danger pill" hidden>
|
||||
<span class="ico">◼</span> Interrompi
|
||||
</button>
|
||||
</div>
|
||||
<div class="beatport-header" style="margin-top:12px;gap:16px;">
|
||||
<span id="catalog-source-display" class="beatport-output-info" style="margin:0;">Nessuna cartella selezionata</span>
|
||||
</div>
|
||||
<div class="beatport-header" style="margin-top:12px;gap:16px;">
|
||||
<label style="display:flex;gap:6px;align-items:center;color:var(--text-2);font-size:13px;">
|
||||
<input type="checkbox" id="catalog-recursive" checked />
|
||||
<span>Ricerca ricorsiva (include sottocartelle)</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2 class="section-label">Destinazione</h2>
|
||||
<div class="card">
|
||||
<div class="beatport-header">
|
||||
<button id="catalog-pick-target" class="btn btn-ghost pill">📁 Cartella destinazione</button>
|
||||
<button id="catalog-clear-target" class="btn btn-ghost pill btn-sm">Usa la stessa della sorgente</button>
|
||||
</div>
|
||||
<div class="beatport-header" style="margin-top:12px;gap:16px;">
|
||||
<span id="catalog-target-display" class="beatport-output-info" style="margin:0;">Uguale alla sorgente (in-place)</span>
|
||||
</div>
|
||||
<div class="hint" style="margin-top:10px;">
|
||||
<span class="hint-ico">ⓘ</span>
|
||||
<div>
|
||||
Se lasci vuota la destinazione i file vengono organizzati <strong>in-place</strong> nella cartella sorgente.
|
||||
Le sottocartelle <code><Anno>/<Genere>/</code> vengono create automaticamente.
|
||||
File senza match AcoustID finiscono in <code>Unknown Year/Unknown Genre/</code>.
|
||||
</div>
|
||||
</div>
|
||||
<div id="catalog-status" class="beatport-status"></div>
|
||||
</div>
|
||||
|
||||
<h2 class="section-label">Progresso</h2>
|
||||
<div class="card">
|
||||
<div class="progress">
|
||||
<div class="progress-bar"><div id="catalogProgressFill" class="progress-fill purple"></div></div>
|
||||
<div class="progress-meta">
|
||||
<span id="catalogPercent" class="percent purple">0%</span>
|
||||
<span id="catalogCounter">In attesa</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2 class="section-label">Risultati</h2>
|
||||
<div class="card" id="catalog-summary-card" hidden>
|
||||
<div class="catalog-summary" id="catalog-summary-line"></div>
|
||||
</div>
|
||||
<div id="catalog-table-wrap" class="catalog-scroll" hidden>
|
||||
<table class="catalog-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="catalog-col-check">
|
||||
<input type="checkbox" id="catalog-select-all" title="Seleziona/deseleziona tutti"/>
|
||||
</th>
|
||||
<th class="catalog-col-file">File</th>
|
||||
<th class="catalog-col-artist">Artista</th>
|
||||
<th class="catalog-col-title">Titolo</th>
|
||||
<th class="catalog-col-year">Anno</th>
|
||||
<th class="catalog-col-genre">Genere</th>
|
||||
<th class="catalog-col-status">Stato</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="catalog-table-body"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="catalog-footer-bar" id="catalog-footer-bar" hidden>
|
||||
<span class="counter" id="catalog-selected-count">0 file selezionati</span>
|
||||
<button id="catalog-move-btn" class="btn btn-danger pill" disabled>
|
||||
<span class="ico">📦</span> Sposta selezionati
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<h2 class="section-label">Log</h2>
|
||||
<div class="card log-card">
|
||||
<div class="log" id="catalog-log"></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</main>
|
||||
</div>
|
||||
|
||||
@@ -1397,6 +1501,29 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ============== MODAL: Cataloga — conferma spostamento ============== -->
|
||||
<div class="modal-backdrop" id="catalogConfirmModal" hidden>
|
||||
<div class="modal">
|
||||
<div class="modal-head">
|
||||
<h2>Conferma spostamento</h2>
|
||||
<button class="modal-close" id="catalogConfirmCloseBtn">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p id="catalogConfirmMsg">Stai per spostare N file. Procedi?</p>
|
||||
<p class="modal-hint">
|
||||
L'operazione riorganizza i file in sottocartelle
|
||||
<code><Anno>/<Genere>/</code>. E' reversibile
|
||||
manualmente dal log ma puo' richiedere tempo su cartelle
|
||||
grandi.
|
||||
</p>
|
||||
</div>
|
||||
<div class="modal-foot">
|
||||
<button class="btn btn-ghost pill" id="catalogConfirmCancelBtn">Annulla</button>
|
||||
<button class="btn btn-danger pill" id="catalogConfirmProceedBtn">Sposta</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ============== TOAST ============== -->
|
||||
<div class="toast" id="toast"></div>
|
||||
|
||||
|
||||
+438
@@ -409,6 +409,7 @@ const logEls = {
|
||||
convert: () => $("#convert-log"),
|
||||
traxsource: () => $("#traxsource-log"),
|
||||
dedup: () => $("#dedup-log"),
|
||||
catalog: () => $("#catalog-log"),
|
||||
};
|
||||
|
||||
function classifyLog(msg) {
|
||||
@@ -530,6 +531,9 @@ async function init() {
|
||||
|
||||
// Dedup tab (audio duplicati via Chromaprint)
|
||||
await DedupUI.init();
|
||||
|
||||
// Cataloga tab (AcoustID -> <Anno>/<Genere>/)
|
||||
await CatalogUI.init();
|
||||
}
|
||||
|
||||
async function refreshRecDevices() {
|
||||
@@ -3205,6 +3209,440 @@ const DedupUI = (() => {
|
||||
return { init };
|
||||
})();
|
||||
|
||||
// ============================================================
|
||||
// CatalogUI — AcoustID -> <Anno>/<Genere>/
|
||||
// ============================================================
|
||||
const CatalogUI = (() => {
|
||||
const cstate = {
|
||||
source: "",
|
||||
target: "",
|
||||
recursive: true,
|
||||
scanning: false,
|
||||
// Map path -> entry (mantiene ordine di inserimento = streaming order)
|
||||
entries: new Map(),
|
||||
// Set di path selezionati per il move
|
||||
toMove: new Set(),
|
||||
};
|
||||
|
||||
function setStatus(text, kind) {
|
||||
const el = $("#catalog-status");
|
||||
if (!el) return;
|
||||
el.textContent = text || "";
|
||||
el.className = "beatport-status" + (kind ? " " + kind : "");
|
||||
}
|
||||
|
||||
function _effectiveTarget() {
|
||||
return cstate.target || cstate.source || "";
|
||||
}
|
||||
|
||||
function updateTargetDisplay() {
|
||||
const el = $("#catalog-target-display");
|
||||
if (!el) return;
|
||||
if (cstate.target) {
|
||||
el.textContent = cstate.target;
|
||||
} else if (cstate.source) {
|
||||
el.textContent = "Uguale alla sorgente: " + cstate.source;
|
||||
} else {
|
||||
el.textContent = "Uguale alla sorgente (in-place)";
|
||||
}
|
||||
}
|
||||
|
||||
function updateSelectedCount() {
|
||||
const n = cstate.toMove.size;
|
||||
const cnt = $("#catalog-selected-count");
|
||||
if (cnt) cnt.textContent = n === 1 ? "1 file selezionato" : `${n} file selezionati`;
|
||||
const btn = $("#catalog-move-btn");
|
||||
if (btn) btn.disabled = n === 0 || cstate.scanning;
|
||||
const bar = $("#catalog-footer-bar");
|
||||
if (bar) bar.hidden = cstate.entries.size === 0;
|
||||
}
|
||||
|
||||
function _statusBadge(entry) {
|
||||
if (entry.error) {
|
||||
return `<span class="catalog-status-badge err" title="${_escapeHtml(entry.error)}">errore</span>`;
|
||||
}
|
||||
if (entry.matched && entry.year && entry.genre) {
|
||||
return `<span class="catalog-status-badge ok">match</span>`;
|
||||
}
|
||||
if (entry.matched) {
|
||||
// Match parziale (mancano anno o genere)
|
||||
return `<span class="catalog-status-badge warn">parziale</span>`;
|
||||
}
|
||||
return `<span class="catalog-status-badge warn">sconosciuto</span>`;
|
||||
}
|
||||
|
||||
function _rowShouldBePreselected(entry) {
|
||||
// Preselezione: solo match validi (year AND genre presenti).
|
||||
return !!(entry.matched && entry.year && entry.genre);
|
||||
}
|
||||
|
||||
function _renderRow(entry) {
|
||||
const path = entry.path;
|
||||
const tr = document.createElement("tr");
|
||||
tr.dataset.path = path;
|
||||
const unmatched = !entry.matched || !entry.year || !entry.genre;
|
||||
if (unmatched) tr.classList.add("catalog-row-unmatched");
|
||||
|
||||
// Checkbox
|
||||
const tdCheck = document.createElement("td");
|
||||
tdCheck.className = "catalog-col-check";
|
||||
const cb = document.createElement("input");
|
||||
cb.type = "checkbox";
|
||||
cb.className = "catalog-check";
|
||||
const pre = _rowShouldBePreselected(entry);
|
||||
cb.checked = pre;
|
||||
if (pre) cstate.toMove.add(path);
|
||||
cb.addEventListener("change", () => {
|
||||
if (cb.checked) cstate.toMove.add(path);
|
||||
else cstate.toMove.delete(path);
|
||||
updateSelectedCount();
|
||||
});
|
||||
tdCheck.appendChild(cb);
|
||||
tr.appendChild(tdCheck);
|
||||
|
||||
// File (basename)
|
||||
const tdFile = document.createElement("td");
|
||||
tdFile.className = "catalog-col-file";
|
||||
tdFile.innerHTML = `<span class="catalog-file-cell" title="${_escapeHtml(path)}">${_escapeHtml(_basename(path))}</span>`;
|
||||
tr.appendChild(tdFile);
|
||||
|
||||
// Artista
|
||||
const tdArtist = document.createElement("td");
|
||||
tdArtist.className = "catalog-col-artist";
|
||||
tdArtist.textContent = entry.artist || "—";
|
||||
tdArtist.title = entry.artist || "";
|
||||
tr.appendChild(tdArtist);
|
||||
|
||||
// Titolo
|
||||
const tdTitle = document.createElement("td");
|
||||
tdTitle.className = "catalog-col-title";
|
||||
tdTitle.textContent = entry.title || "—";
|
||||
tdTitle.title = entry.title || "";
|
||||
tr.appendChild(tdTitle);
|
||||
|
||||
// Anno
|
||||
const tdYear = document.createElement("td");
|
||||
tdYear.className = "catalog-col-year";
|
||||
tdYear.textContent = entry.year ? String(entry.year) : "—";
|
||||
tr.appendChild(tdYear);
|
||||
|
||||
// Genere
|
||||
const tdGenre = document.createElement("td");
|
||||
tdGenre.className = "catalog-col-genre";
|
||||
tdGenre.textContent = entry.genre || "—";
|
||||
tdGenre.title = entry.genre || "";
|
||||
tr.appendChild(tdGenre);
|
||||
|
||||
// Status
|
||||
const tdStatus = document.createElement("td");
|
||||
tdStatus.className = "catalog-col-status";
|
||||
tdStatus.innerHTML = _statusBadge(entry);
|
||||
tr.appendChild(tdStatus);
|
||||
|
||||
return tr;
|
||||
}
|
||||
|
||||
function _upsertEntry(entry) {
|
||||
if (!entry || !entry.path) return;
|
||||
const wrap = $("#catalog-table-wrap");
|
||||
const body = $("#catalog-table-body");
|
||||
if (!body) return;
|
||||
const existed = cstate.entries.has(entry.path);
|
||||
cstate.entries.set(entry.path, entry);
|
||||
|
||||
if (existed) {
|
||||
// Aggiorna in place: rimuovi vecchia riga e append nuova nella stessa posizione
|
||||
const oldRow = body.querySelector(`tr[data-path="${CSS.escape(entry.path)}"]`);
|
||||
const newRow = _renderRow(entry);
|
||||
if (oldRow) body.replaceChild(newRow, oldRow);
|
||||
else body.appendChild(newRow);
|
||||
} else {
|
||||
body.appendChild(_renderRow(entry));
|
||||
}
|
||||
if (wrap) wrap.hidden = false;
|
||||
_updateSummary();
|
||||
updateSelectedCount();
|
||||
}
|
||||
|
||||
function _updateSummary() {
|
||||
const line = $("#catalog-summary-line");
|
||||
const card = $("#catalog-summary-card");
|
||||
if (!line || !card) return;
|
||||
const n = cstate.entries.size;
|
||||
if (n === 0) { card.hidden = true; return; }
|
||||
let matched = 0, unknown = 0, errors = 0;
|
||||
cstate.entries.forEach((e) => {
|
||||
if (e.error) errors++;
|
||||
else if (e.matched && e.year && e.genre) matched++;
|
||||
else unknown++;
|
||||
});
|
||||
line.textContent =
|
||||
`${n} file · ${matched} identificati · ${unknown} parziali/sconosciuti · ${errors} errori`;
|
||||
card.hidden = false;
|
||||
}
|
||||
|
||||
function _clearTable() {
|
||||
const body = $("#catalog-table-body");
|
||||
if (body) body.innerHTML = "";
|
||||
const wrap = $("#catalog-table-wrap");
|
||||
if (wrap) wrap.hidden = true;
|
||||
const card = $("#catalog-summary-card");
|
||||
if (card) card.hidden = true;
|
||||
cstate.entries.clear();
|
||||
cstate.toMove.clear();
|
||||
updateSelectedCount();
|
||||
}
|
||||
|
||||
async function pickSource() {
|
||||
const p = await window.pywebview.api.catalog_pick_source_folder();
|
||||
if (p && typeof p === "string") {
|
||||
cstate.source = p;
|
||||
$("#catalog-source-display").textContent = p;
|
||||
$("#catalog-start-btn").disabled = false;
|
||||
updateTargetDisplay();
|
||||
setStatus("", "");
|
||||
}
|
||||
}
|
||||
|
||||
async function pickTarget() {
|
||||
const p = await window.pywebview.api.catalog_pick_target_folder();
|
||||
if (p && typeof p === "string") {
|
||||
cstate.target = p;
|
||||
updateTargetDisplay();
|
||||
}
|
||||
}
|
||||
|
||||
function clearTarget() {
|
||||
cstate.target = "";
|
||||
updateTargetDisplay();
|
||||
}
|
||||
|
||||
async function startScan() {
|
||||
if (!cstate.source) return;
|
||||
cstate.recursive = $("#catalog-recursive").checked;
|
||||
cstate.scanning = true;
|
||||
_clearTable();
|
||||
$("#catalog-start-btn").disabled = true;
|
||||
$("#catalog-pick-source").disabled = true;
|
||||
$("#catalog-stop-btn").hidden = false;
|
||||
$("#catalog-log").innerHTML = "";
|
||||
$("#catalogProgressFill").style.width = "0%";
|
||||
$("#catalogPercent").textContent = "0%";
|
||||
$("#catalogCounter").textContent = "Scansione in corso…";
|
||||
setStatus("Scansione in corso...", "loading");
|
||||
|
||||
let res;
|
||||
try {
|
||||
res = await window.pywebview.api.catalog_start_scan({
|
||||
source: cstate.source,
|
||||
target: cstate.target,
|
||||
recursive: cstate.recursive,
|
||||
});
|
||||
} catch (e) {
|
||||
setStatus("Errore avvio: " + ((e && e.message) || e), "error");
|
||||
finishScan();
|
||||
return;
|
||||
}
|
||||
if (!res || !res.ok) {
|
||||
const msg = (res && res.error) || "Impossibile avviare la scansione";
|
||||
setStatus(msg, "error");
|
||||
if (!handleGateBlock(res || {})) toast(msg, "error");
|
||||
finishScan();
|
||||
}
|
||||
}
|
||||
|
||||
async function stopScan() {
|
||||
try { await window.pywebview.api.catalog_stop_scan(); } catch (e) { console.error(e); }
|
||||
}
|
||||
|
||||
function finishScan() {
|
||||
cstate.scanning = false;
|
||||
$("#catalog-start-btn").disabled = !cstate.source;
|
||||
$("#catalog-pick-source").disabled = false;
|
||||
$("#catalog-stop-btn").hidden = true;
|
||||
updateSelectedCount();
|
||||
}
|
||||
|
||||
function _openConfirmModal(nFiles, target) {
|
||||
const msg = $("#catalogConfirmMsg");
|
||||
if (msg) {
|
||||
const word = nFiles === 1 ? "1 file" : `${nFiles} file`;
|
||||
msg.textContent = `Stai per spostare ${word} in ${target}. Procedi?`;
|
||||
}
|
||||
$("#catalogConfirmModal").hidden = false;
|
||||
}
|
||||
|
||||
function _closeConfirmModal() {
|
||||
$("#catalogConfirmModal").hidden = true;
|
||||
}
|
||||
|
||||
async function moveSelected() {
|
||||
const paths = [...cstate.toMove];
|
||||
if (!paths.length) return;
|
||||
const target = _effectiveTarget();
|
||||
if (!target) { toast("Cartella destinazione mancante", "error"); return; }
|
||||
|
||||
// Costruisci lista entries in ordine di selezione
|
||||
const selectedEntries = paths
|
||||
.map((p) => cstate.entries.get(p))
|
||||
.filter(Boolean);
|
||||
if (!selectedEntries.length) return;
|
||||
|
||||
_openConfirmModal(selectedEntries.length, target);
|
||||
|
||||
// Attendi conferma o annulla via bottoni
|
||||
const proceed = await new Promise((resolve) => {
|
||||
const yes = $("#catalogConfirmProceedBtn");
|
||||
const no = $("#catalogConfirmCancelBtn");
|
||||
const close1 = $("#catalogConfirmCloseBtn");
|
||||
const cleanup = () => {
|
||||
yes.removeEventListener("click", onYes);
|
||||
no.removeEventListener("click", onNo);
|
||||
close1.removeEventListener("click", onNo);
|
||||
};
|
||||
const onYes = () => { cleanup(); _closeConfirmModal(); resolve(true); };
|
||||
const onNo = () => { cleanup(); _closeConfirmModal(); resolve(false); };
|
||||
yes.addEventListener("click", onYes);
|
||||
no.addEventListener("click", onNo);
|
||||
close1.addEventListener("click", onNo);
|
||||
});
|
||||
if (!proceed) return;
|
||||
|
||||
const btn = $("#catalog-move-btn");
|
||||
btn.disabled = true;
|
||||
let res;
|
||||
try {
|
||||
res = await window.pywebview.api.catalog_move_files(selectedEntries, target);
|
||||
} catch (e) {
|
||||
toast("Errore: " + ((e && e.message) || e), "error");
|
||||
btn.disabled = false;
|
||||
return;
|
||||
}
|
||||
if (!res || !res.ok) {
|
||||
toast((res && res.error) || "Errore spostamento", "error");
|
||||
btn.disabled = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// Rimuovi dalla tabella le entry spostate (usando operations.src)
|
||||
const moved = new Set((res.operations || []).map((o) => o.src));
|
||||
const body = $("#catalog-table-body");
|
||||
moved.forEach((p) => {
|
||||
cstate.entries.delete(p);
|
||||
cstate.toMove.delete(p);
|
||||
if (body) {
|
||||
const row = body.querySelector(`tr[data-path="${CSS.escape(p)}"]`);
|
||||
if (row) row.remove();
|
||||
}
|
||||
});
|
||||
_updateSummary();
|
||||
updateSelectedCount();
|
||||
const wrap = $("#catalog-table-wrap");
|
||||
if (wrap && cstate.entries.size === 0) wrap.hidden = true;
|
||||
|
||||
const okN = res.moved || 0;
|
||||
const failN = res.failed_count || 0;
|
||||
if (failN > 0) {
|
||||
toast(`${okN} spostati, ${failN} falliti`, "error");
|
||||
} else {
|
||||
toast(`${okN} file organizzati`, "success");
|
||||
}
|
||||
}
|
||||
|
||||
async function init() {
|
||||
// Bind eventi
|
||||
$("#catalog-pick-source").addEventListener("click", pickSource);
|
||||
$("#catalog-pick-target").addEventListener("click", pickTarget);
|
||||
$("#catalog-clear-target").addEventListener("click", clearTarget);
|
||||
$("#catalog-start-btn").addEventListener("click", startScan);
|
||||
$("#catalog-stop-btn").addEventListener("click", stopScan);
|
||||
$("#catalog-move-btn").addEventListener("click", moveSelected);
|
||||
$("#catalog-recursive").addEventListener("change", (e) => {
|
||||
cstate.recursive = e.target.checked;
|
||||
});
|
||||
$("#catalog-select-all").addEventListener("change", (e) => {
|
||||
const on = e.target.checked;
|
||||
const checks = $$("#catalog-table-body .catalog-check");
|
||||
cstate.toMove.clear();
|
||||
checks.forEach((cb) => {
|
||||
cb.checked = on;
|
||||
if (on) {
|
||||
const row = cb.closest("tr");
|
||||
if (row && row.dataset.path) cstate.toMove.add(row.dataset.path);
|
||||
}
|
||||
});
|
||||
updateSelectedCount();
|
||||
});
|
||||
|
||||
// Ripristina ultimo state da config
|
||||
const cfg = state.config || {};
|
||||
if (cfg.catalog_last_source) {
|
||||
cstate.source = cfg.catalog_last_source;
|
||||
$("#catalog-source-display").textContent = cfg.catalog_last_source;
|
||||
$("#catalog-start-btn").disabled = false;
|
||||
}
|
||||
if (cfg.catalog_last_target) {
|
||||
cstate.target = cfg.catalog_last_target;
|
||||
}
|
||||
if (typeof cfg.catalog_recursive === "boolean") {
|
||||
$("#catalog-recursive").checked = cfg.catalog_recursive;
|
||||
cstate.recursive = cfg.catalog_recursive;
|
||||
}
|
||||
updateTargetDisplay();
|
||||
|
||||
// Bridge handlers
|
||||
bridgeHandlers["catalog:progress"] = (p) => {
|
||||
if (!p) return;
|
||||
if (typeof p.overall === "number") {
|
||||
const pct = Math.round(p.overall * 100);
|
||||
$("#catalogProgressFill").style.width = pct + "%";
|
||||
$("#catalogPercent").textContent = pct + "%";
|
||||
}
|
||||
if (typeof p.idx === "number" && typeof p.total === "number" && p.total > 0) {
|
||||
const status = p.status || "";
|
||||
let label;
|
||||
if (status === "computing") label = `File ${p.idx}/${p.total} · fingerprint`;
|
||||
else if (status === "lookup") label = `File ${p.idx}/${p.total} · AcoustID`;
|
||||
else if (status === "completed") label = `Completato: ${p.total} file`;
|
||||
else if (status === "stopped") label = "Interrotta";
|
||||
else if (status === "error") label = `File ${p.idx}/${p.total} · errore`;
|
||||
else label = `File ${p.idx}/${p.total}`;
|
||||
$("#catalogCounter").textContent = label;
|
||||
}
|
||||
};
|
||||
|
||||
bridgeHandlers["catalog:entry"] = (entry) => {
|
||||
_upsertEntry(entry);
|
||||
setStatus(`Elaborati ${cstate.entries.size} file...`, "loading");
|
||||
};
|
||||
|
||||
bridgeHandlers["catalog:log"] = (_op) => {
|
||||
// Log dettagliato gia' appeso via il canale "log" dal backend.
|
||||
// Handler dedicato riservato per estensioni future (es. undo).
|
||||
};
|
||||
|
||||
bridgeHandlers["catalog:done"] = (p) => {
|
||||
// Se lo streaming non ha popolato nulla (raro, es. errore), usa
|
||||
// p.entries come fallback.
|
||||
if (cstate.entries.size === 0 && p && Array.isArray(p.entries)) {
|
||||
p.entries.forEach(_upsertEntry);
|
||||
}
|
||||
if (p && p.ok === false) {
|
||||
setStatus(p.error || "Errore scansione", "error");
|
||||
} else if (cstate.entries.size === 0) {
|
||||
setStatus("Nessun file audio trovato", "ok");
|
||||
} else {
|
||||
const matched = (p && p.n_matched) || 0;
|
||||
const total = (p && p.n_total) || cstate.entries.size;
|
||||
setStatus(`Scansione completata · ${matched}/${total} identificati`, "ok");
|
||||
}
|
||||
finishScan();
|
||||
};
|
||||
}
|
||||
|
||||
return { init };
|
||||
})();
|
||||
|
||||
// ============================================================
|
||||
// Boot
|
||||
// ============================================================
|
||||
|
||||
Reference in new issue
Block a user