From a774906814e33ead4bb80dc924bcf49644057d1e Mon Sep 17 00:00:00 2001 From: luciano Date: Tue, 21 Jul 2026 17:48:31 +0200 Subject: [PATCH] ui: ConvertUI (file picker, opzioni, batch conversione) --- webui/js/app.js | 226 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 226 insertions(+) diff --git a/webui/js/app.js b/webui/js/app.js index 02fd1d5..b99df9a 100644 --- a/webui/js/app.js +++ b/webui/js/app.js @@ -399,6 +399,7 @@ const logEls = { video: () => $("#videoLog"), spotify: () => $("#spotify-log"), youtube: () => $("#youtube-log"), + convert: () => $("#convert-log"), }; function classifyLog(msg) { @@ -510,6 +511,9 @@ async function init() { // Spotify + YouTube search tabs await SpotifyUI.init(); await YoutubeUI.init(); + + // WAV -> MP3 converter tab + await ConvertUI.init(); } async function refreshRecDevices() { @@ -1948,6 +1952,228 @@ const YoutubeUI = (() => { return { init }; })(); +// ===================================================================== +// Converti (WAV -> MP3) +// ===================================================================== +const ConvertUI = (() => { + const mstate = { files: [], selected: new Set(), running: false, outputDir: "" }; + + function _escape(s) { + const d = document.createElement("div"); + d.textContent = s == null ? "" : String(s); + return d.innerHTML; + } + + function renderTable() { + const tbody = $("#convert-tbody"); + if (!tbody) return; + tbody.innerHTML = ""; + mstate.files.forEach((f, i) => { + const tr = document.createElement("tr"); + tr.dataset.idx = String(i); + const checked = mstate.selected.has(i) ? "checked" : ""; + const baseName = f.split(/[\/\\]/).pop(); + tr.innerHTML = ` + + ${i + 1} + ${_escape(baseName)} + + `; + tr.querySelector("input[type=checkbox]").addEventListener("change", (e) => { + if (e.target.checked) mstate.selected.add(i); + else mstate.selected.delete(i); + updateSelectionCount(); + }); + tbody.appendChild(tr); + }); + $("#convert-table-wrap").hidden = mstate.files.length === 0; + updateSelectionCount(); + } + + function updateSelectionCount() { + const total = mstate.files.length; + const sel = mstate.selected.size; + $("#convert-selected-count").textContent = `${sel}/${total} selezionati`; + $("#convert-run-btn").disabled = mstate.running || sel === 0; + const sa = $("#convert-select-all"); + if (sa) { + sa.indeterminate = sel > 0 && sel < total; + sa.checked = sel === total && total > 0; + } + } + + function appendFiles(newFiles) { + // Dedup + append + const existing = new Set(mstate.files); + for (const f of newFiles) { + if (!existing.has(f)) { + mstate.files.push(f); + mstate.selected.add(mstate.files.length - 1); + existing.add(f); + } + } + renderTable(); + } + + function setStatus(text, kind) { + const el = $("#convert-status"); + if (!el) return; + el.textContent = text || ""; + el.className = "beatport-status" + (kind ? " " + kind : ""); + } + + async function pickFiles() { + const files = await window.pywebview.api.convert_pick_wav_files(); + if (Array.isArray(files) && files.length) { + appendFiles(files); + setStatus("", ""); + } else { + setStatus("Nessun file .wav selezionato", ""); + } + } + + async function pickFolder() { + setStatus("Scansione cartella...", "loading"); + const files = await window.pywebview.api.convert_pick_wav_folder(true); + if (Array.isArray(files) && files.length) { + appendFiles(files); + setStatus(`Trovati ${files.length} file .wav`, "ok"); + } else { + setStatus("Nessun file .wav trovato nella cartella", ""); + } + } + + function clearFiles() { + mstate.files = []; + mstate.selected.clear(); + renderTable(); + setStatus("", ""); + $("#convert-log").innerHTML = ""; + } + + async function browseOutput() { + const p = await window.pywebview.api.browse_directory(); + if (p && typeof p === "string") { + mstate.outputDir = p; + $("#convert-out-path").textContent = p; + $("#convert-out-custom").checked = true; + } + } + + function appendConvertLog(msg) { + const el = $("#convert-log"); + if (!el) return; + const cls = classifyLog(msg); + const line = document.createElement("div"); + if (cls) line.className = cls; + line.textContent = msg; + el.appendChild(line); + el.scrollTop = el.scrollHeight; + } + + function updateStateCell(idx, text, cls) { + const td = document.querySelector(`#convert-tbody td.col-state[data-idx="${idx}"]`); + if (!td) return; + td.textContent = text; + td.style.color = cls === "err" ? "var(--red)" : (cls === "ok" ? "var(--green-text)" : ""); + } + + async function startConversion() { + const files = [...mstate.selected].sort((a, b) => a - b).map((i) => mstate.files[i]); + if (!files.length) return; + const bitrate = parseInt($("#convert-bitrate").value, 10); + const vbr = $("#convert-vbr").checked; + const useCustom = $("#convert-out-custom").checked; + const output_dir = useCustom ? (mstate.outputDir || "") : ""; + if (useCustom && !output_dir) { + setStatus("Scegli una cartella di destinazione", "error"); + return; + } + + mstate.running = true; + $("#convert-run-btn").disabled = true; + $("#convert-stop-btn").hidden = false; + $("#convert-log").innerHTML = ""; + // Reset state cells + $$("#convert-tbody td.col-state").forEach((td) => { + td.textContent = ""; + td.style.color = ""; + }); + + let res; + try { + res = await window.pywebview.api.convert_start({ files, bitrate, vbr, output_dir }); + } catch (e) { + appendConvertLog("[ERRORE] " + ((e && e.message) || e)); + finishConversion(); + return; + } + if (!res || !res.ok) { + const errMsg = (res && res.error) || "Impossibile avviare"; + appendConvertLog("[ERRORE] " + errMsg); + if (!handleGateBlock(res || {})) toast(errMsg, "error"); + finishConversion(); + } + } + + async function stopConversion() { + try { await window.pywebview.api.convert_stop(); } catch (e) { console.error(e); } + } + + function finishConversion() { + mstate.running = false; + $("#convert-run-btn").disabled = mstate.selected.size === 0; + $("#convert-stop-btn").hidden = true; + } + + async function init() { + // Bind eventi + $("#convert-pick-files").addEventListener("click", pickFiles); + $("#convert-pick-folder").addEventListener("click", pickFolder); + $("#convert-clear").addEventListener("click", clearFiles); + $("#convert-out-browse").addEventListener("click", browseOutput); + $("#convert-out-custom").addEventListener("change", () => { + $("#convert-out-browse").disabled = false; + }); + $("#convert-out-same").addEventListener("change", () => { + $("#convert-out-browse").disabled = true; + }); + $("#convert-select-all").addEventListener("change", (e) => { + mstate.selected.clear(); + if (e.target.checked) { + for (let i = 0; i < mstate.files.length; i++) mstate.selected.add(i); + } + renderTable(); + }); + $("#convert-run-btn").addEventListener("click", startConversion); + $("#convert-stop-btn").addEventListener("click", stopConversion); + + // Bridge handlers per il canale "convert" + const origProgress = bridgeHandlers["convert:progress"]; + bridgeHandlers["convert:progress"] = (p) => { + if (origProgress) origProgress(p); + if (!p) return; + if (p.status === "done") updateStateCell(p.idx, "✓ fatto", "ok"); + else if (p.status === "skipped") updateStateCell(p.idx, "già esiste", ""); + else if (p.status === "stopped") updateStateCell(p.idx, "interrotto", ""); + else if (typeof p.status === "string" && p.status.startsWith("error")) updateStateCell(p.idx, "✗ errore", "err"); + else if (p.status === "converting") updateStateCell(p.idx, `${p.pct || 0}%`, ""); + }; + const origDone = bridgeHandlers["convert:done"]; + bridgeHandlers["convert:done"] = (p) => { + if (origDone) origDone(p); + if (mstate.running) { + appendConvertLog("[INFO] Conversione terminata."); + finishConversion(); + } + }; + // Il canale "log" e' gia gestito da logEls.convert (aggiunto sopra), quindi + // non serve wrap qui: appendLog(view=convert, msg) targeta #convert-log. + } + + return { init }; +})(); + // ============================================================ // Boot // ============================================================