feat: upgrade archivio con auto-pick, audio preview, cancel-all (v1.9.4)

Tab Upgrade:
- Checkbox "Scelta automatica" per skippare il modal quando ci sono
  multi-match (usa bitrate DESC)
- Anteprima audio nel picker: play button sul source + ogni candidato
  via bridge read_audio_data_url (WKWebView blocca file://)
- Bottone "Annulla tutto" nel picker → interrompe l'intera coda
- Fix mtime: shutil.copy (non copy2) così il file upgradato ha
  data odierna nel Finder
- Fix stop audio → non triggera più toast di errore
- Modal picker più largo (620-780px) con footer wrap

Extra: campo release_date estratto anche in Beatport (publish_date)
e YouTube (upload_date) — completato scaffold sort colonna Data
This commit is contained in:
luciano committed 2026-08-02 12:56:19 +02:00
1 parent e10466d93b
commit e7fde842f2
8 files changed
+219 -13

No files matched your search

+51
View File
@@ -1680,8 +1680,21 @@ input[type="number"]::-webkit-inner-spin-button {
}
.beatport-table .col-pos { width: 40px; color: var(--text-3); font-variant-numeric: tabular-nums; }
.beatport-table .col-dur { width: 70px; color: var(--text-2); font-variant-numeric: tabular-nums; text-align: right; }
.beatport-table .col-date { width: 100px; color: var(--text-2); font-variant-numeric: tabular-nums; font-size: 12px; white-space: nowrap; }
.beatport-table .col-state { width: 130px; font-size: 12px; }
.beatport-table thead th.sortable {
cursor: pointer;
user-select: none;
transition: color 0.15s ease;
}
.beatport-table thead th.sortable:hover { color: var(--text); }
.beatport-table thead th .sort-arrow {
font-size: 10px;
margin-left: 4px;
color: var(--indigo-text);
}
.beatport-table input[type="checkbox"] {
width: 16px;
height: 16px;
@@ -1720,6 +1733,17 @@ input[type="number"]::-webkit-inner-spin-button {
white-space: normal;
user-select: none;
}
/* Modal picker: più largo dei modal standard + footer con wrap
per far entrare i 4 bottoni senza troncare */
#upgradePickerModal .modal {
min-width: 620px;
max-width: 780px;
}
#upgradePickerModal .modal-foot {
flex-wrap: wrap;
justify-content: flex-end;
}
.modal-sub {
font-size: 12px;
color: var(--text-2);
@@ -1757,6 +1781,33 @@ input[type="number"]::-webkit-inner-spin-button {
flex: 1;
min-width: 0;
}
.upgrade-picker-play {
display: flex;
align-items: center;
flex-shrink: 0;
}
.upgrade-preview-btn {
width: 34px;
height: 34px;
border-radius: 50%;
border: 1px solid var(--border);
background: var(--bg-input, transparent);
color: var(--text);
font-size: 13px;
cursor: pointer;
display: inline-flex;
align-items: center;
justify-content: center;
padding: 0;
margin-left: 6px;
transition: background 0.15s ease, transform 0.1s ease;
}
.upgrade-preview-btn:hover { background: var(--bg-input-hover, rgba(255,255,255,0.05)); }
.upgrade-preview-btn.playing {
background: var(--green, #1DB954);
color: #000;
border-color: var(--green, #1DB954);
}
.upgrade-picker-title {
font-weight: 600;
color: var(--text);
+7 -2
View File
@@ -280,9 +280,13 @@
<button class="btn btn-ghost pill btn-sm" id="upArchiveBrowseBtn">📂 Sfoglia</button>
<button class="btn btn-ghost pill btn-sm" id="upArchiveClearBtn" hidden>Rimuovi</button>
</div>
<label style="display:flex;gap:6px;align-items:center;margin-top:8px;font-size:13px;color:var(--text-2);">
<input type="checkbox" id="upArchiveAutoPick"/>
<span>Scelta automatica se ci sono più match (usa quello a bitrate più alto)</span>
</label>
<div class="hint">
<span class="hint-ico">ⓘ</span>
<div>Se selezionata, l'app cerca versioni HQ del brano nella tua libreria locale prima di scaricare da YouTube. In caso di match multipli scegli tu quale usare.</div>
<div>Se selezionata, l'app cerca versioni HQ del brano nella tua libreria locale prima di scaricare da YouTube. In caso di match multipli scegli tu quale usare — oppure attiva "Scelta automatica" per non fermarti.</div>
</div>
</div>
</div>
@@ -1289,7 +1293,8 @@
<div class="modal-foot">
<button class="btn btn-primary pill" id="upgradePickerUseBtn" disabled>Usa selezionato</button>
<button class="btn btn-ghost pill" id="upgradePickerYoutubeBtn">Cerca su YouTube</button>
<button class="btn btn-danger pill" id="upgradePickerSkipBtn">Salta</button>
<button class="btn btn-ghost pill" id="upgradePickerSkipBtn">Salta</button>
<button class="btn btn-danger pill" id="upgradePickerCancelAllBtn">■ Annulla tutto</button>
</div>
</div>
</div>
+100 -3
View File
@@ -776,6 +776,7 @@ $("#upgradeBtn").addEventListener("click", async () => {
recursive: $("#upRecursive").checked,
threshold: parseInt($("#upThreshold").value, 10) || 310,
archive_dir: state.upArchiveDir || "",
archive_auto_pick: $("#upArchiveAutoPick").checked,
});
if (!res.ok) {
if (!handleGateBlock(res)) toast(res.error || "Errore", "error");
@@ -809,14 +810,98 @@ function _escapeHtml(s) {
}[c]));
}
// --- Audio preview per il picker (single audio element condiviso) ---
let _upgradePreviewAudio = null;
let _upgradePreviewBtn = null; // bottone corrente in "playing" state
function _fileUrl(absPath) {
if (!absPath) return "";
// Split percorso e encode ogni segment (spazi → %20, unicode → utf-8 encoded)
const parts = String(absPath).split("/").map(encodeURIComponent);
return "file://" + parts.join("/");
}
function _stopUpgradePreview() {
if (_upgradePreviewAudio) {
// Rimuovi handler error PRIMA di pause/clear così stop manuale
// non fa scattare il toast di errore
_upgradePreviewAudio.onerror = null;
_upgradePreviewAudio.onended = null;
try { _upgradePreviewAudio.pause(); } catch {}
try { _upgradePreviewAudio.removeAttribute("src"); } catch {}
_upgradePreviewAudio = null;
}
if (_upgradePreviewBtn) {
_upgradePreviewBtn.textContent = "▶";
_upgradePreviewBtn.classList.remove("playing");
_upgradePreviewBtn = null;
}
}
function _makePreviewBtn(absPath) {
const btn = document.createElement("button");
btn.type = "button";
btn.className = "upgrade-preview-btn";
btn.textContent = "▶";
btn.title = "Anteprima audio";
btn.addEventListener("click", async (e) => {
e.preventDefault();
e.stopPropagation();
// Se già in play su questo bottone → stop
if (_upgradePreviewBtn === btn) {
_stopUpgradePreview();
return;
}
_stopUpgradePreview();
btn.textContent = "⏳";
btn.disabled = true;
let res;
try {
res = await window.pywebview.api.read_audio_data_url(absPath);
} catch (err) {
btn.textContent = "▶";
btn.disabled = false;
toast && toast("Errore lettura file: " + err, "error");
return;
}
btn.disabled = false;
if (!res || !res.ok) {
btn.textContent = "▶";
toast && toast(res && res.error || "Impossibile leggere il file", "error");
return;
}
_upgradePreviewAudio = new Audio(res.data_url);
_upgradePreviewBtn = btn;
btn.textContent = "■";
btn.classList.add("playing");
_upgradePreviewAudio.onended = _stopUpgradePreview;
_upgradePreviewAudio.onerror = () => {
// Solo se il listener è ancora agganciato (stop manuale lo azzera prima)
const wasPlaying = !!_upgradePreviewAudio;
_stopUpgradePreview();
if (wasPlaying) toast && toast("Errore riproduzione audio", "error");
};
_upgradePreviewAudio.play().catch(() => _stopUpgradePreview());
});
return btn;
}
function openUpgradePicker(payload) {
const modal = $("#upgradePickerModal");
if (!modal) return;
const file = (payload && payload.file) || "";
const sourcePath = (payload && payload.source_path) || "";
const candidates = (payload && payload.candidates) || [];
state.upPicker = { file, candidates, selectedIdx: -1 };
state.upPicker = { file, sourcePath, candidates, selectedIdx: -1 };
// Header: filename + play button per il source
const header = $("#upgradePickerFilename");
header.innerHTML = "";
const label = document.createElement("span");
label.textContent = "Brano da upgradare: " + file + " ";
header.appendChild(label);
if (sourcePath) header.appendChild(_makePreviewBtn(sourcePath));
$("#upgradePickerFilename").textContent = "Brano: " + file;
const list = $("#upgradePickerList");
list.innerHTML = "";
candidates.forEach((c, i) => {
@@ -832,8 +917,13 @@ function openUpgradePicker(payload) {
<div class="upgrade-picker-meta">${_escapeHtml(kbps)} · ${_escapeHtml(_fmtBytes(c.size))} · similarity ${simPct}%</div>
<div class="upgrade-picker-path">${_escapeHtml(c.path || "")}</div>
</div>
<span class="upgrade-picker-play"></span>
`;
row.addEventListener("click", () => {
const playSlot = row.querySelector(".upgrade-picker-play");
if (playSlot && c.path) playSlot.appendChild(_makePreviewBtn(c.path));
row.addEventListener("click", (e) => {
// Se il click viene dal bottone play, non selezionare la riga
if (e.target.closest(".upgrade-preview-btn")) return;
state.upPicker.selectedIdx = i;
$("#upgradePickerUseBtn").disabled = false;
$$("#upgradePickerList .upgrade-picker-row").forEach((r) =>
@@ -849,6 +939,7 @@ function openUpgradePicker(payload) {
}
function closeUpgradePicker() {
_stopUpgradePreview();
const modal = $("#upgradePickerModal");
if (modal) modal.hidden = true;
}
@@ -875,6 +966,12 @@ $("#upgradePickerSkipBtn")?.addEventListener("click", () => {
_sendUpgradeChoice({ action: "skip" });
});
$("#upgradePickerCancelAllBtn")?.addEventListener("click", async () => {
// Skip questo file per sbloccare il worker, poi ferma l'intera coda
_sendUpgradeChoice({ action: "skip" });
try { await window.pywebview.api.stop_upgrade(); } catch {}
});
$("#upgradePickerCloseBtn")?.addEventListener("click", () => {
_sendUpgradeChoice({ action: "skip" });
});