// ---- Library Issues ---- let lastLibraryIssues = null; let _libraryIssuesDirty = false; function renderLibraryIssues(r) { const out = document.getElementById("library-issues-modal-body"); const statusEl = document.getElementById("library-issues-results"); const renameAllBtn = document.getElementById("library-issues-rename-all"); const renameStatus = document.getElementById("library-issues-rename-status"); if (!r || !r.ok) { lastLibraryIssues = null; renameAllBtn.disabled = true; out.innerHTML = `
Error: ${escapeHtml(r?.error || "no response")}
`; openModal("library-issues-modal"); return; } lastLibraryIssues = r; const brackets = r.bracket_names || []; const nohyphens = r.nohyphen_names || []; const total = brackets.length + nohyphens.length; renameAllBtn.disabled = total === 0; renameStatus.textContent = ""; const parts = []; if (!total) { parts.push(`
✓ No library issues found. All filenames are canonical.
`); } else { parts.push(`
${total} file${total !== 1 ? "s" : ""} with non-canonical names — ${brackets.length} bracket-wrapped, ${nohyphens.length} no-hyphen
`); const makeRow = (entry, tagClass, tagLabel) => { const fname = entry.path.split("/").pop(); const dir = entry.path.lastIndexOf("/") !== -1 ? entry.path.slice(0, entry.path.lastIndexOf("/") + 1) : ""; return `
${tagLabel}
${escapeHtml(fname)} → ${escapeHtml(entry.canonical_name)}
${escapeHtml(entry.size_human || "")}
`; }; if (brackets.length) { parts.push(`
Bracket-wrapped IDs (${brackets.length})
`); parts.push(brackets.map((e) => makeRow(e, "bracket", "[ ]")).join("")); } if (nohyphens.length) { parts.push(`
No-hyphen IDs (${nohyphens.length})
`); parts.push(nohyphens.map((e) => makeRow(e, "nohyphen", "no hyphen")).join("")); } } out.innerHTML = parts.join(""); statusEl.textContent = total ? `${total} library issue(s) found. Review window is open.` : "No library issues found."; openModal("library-issues-modal"); // Per-row rename buttons out.querySelectorAll(".li-rename-btn").forEach((btn) => { btn.addEventListener("click", async () => { const row = btn.closest(".li-row"); const remote = row.dataset.remote; const oldPath = row.dataset.old; const newPath = row.dataset.new; btn.disabled = true; btn.textContent = "…"; const res = await chrome.runtime.sendMessage({ type: "rename_file", remote, old_path: oldPath, new_path: newPath, }); const tag = row.querySelector(".li-tag"); if (res?.ok) { tag.className = "li-tag done"; tag.textContent = "✓"; btn.textContent = "Done"; row.querySelector(".li-old").style.textDecoration = "line-through"; _libraryIssuesDirty = true; } else if (res?.conflict) { tag.className = "li-tag conflict"; tag.textContent = "conflict"; btn.textContent = "Skip"; renameStatus.textContent = `Conflict: ${res.error || "target exists"}`; } else { tag.className = "li-tag conflict"; tag.textContent = "error"; btn.textContent = "Error"; renameStatus.textContent = res?.error || "rename failed"; } }); }); } document.getElementById("library-issues-run").addEventListener("click", async () => { const out = document.getElementById("library-issues-modal-body"); out.innerHTML = `
Loading library issues from cache…
`; openModal("library-issues-modal"); renderLibraryIssues(await chrome.runtime.sendMessage({ type: "library_issues" })); }); document.getElementById("library-issues-rename-all").addEventListener("click", async () => { const rows = [...document.querySelectorAll("#library-issues-modal-body .li-row")]; const renameStatus = document.getElementById("library-issues-rename-status"); const renameAllBtn = document.getElementById("library-issues-rename-all"); // Collect pending renames (skip already-done or disabled rows) const pending = rows.reduce((acc, row) => { const btn = row.querySelector(".li-rename-btn"); if (!btn || btn.disabled) return acc; acc.push({ row, remote: row.dataset.remote, old_path: row.dataset.old, new_path: row.dataset.new }); return acc; }, []); if (!pending.length) return; renameAllBtn.disabled = true; renameStatus.textContent = `Renaming ${pending.length} file(s)…`; const renames = pending.map(({ remote, old_path, new_path }) => ({ remote, old_path, new_path })); const res = await chrome.runtime.sendMessage({ type: "rename_files_batch", renames }); const results = res?.results || []; let done = 0, conflicts = 0, errors = 0; results.forEach((r, i) => { const { row } = pending[i]; const tag = row.querySelector(".li-tag"); const btn = row.querySelector(".li-rename-btn"); if (r.ok) { tag.className = "li-tag done"; tag.textContent = "✓"; btn.disabled = true; btn.textContent = "Done"; row.querySelector(".li-old").style.textDecoration = "line-through"; done++; } else if (r.conflict) { tag.className = "li-tag conflict"; tag.textContent = "conflict"; btn.disabled = false; btn.textContent = "Skip"; conflicts++; } else { tag.className = "li-tag conflict"; tag.textContent = "error"; btn.disabled = false; btn.textContent = "Error"; errors++; } }); const parts = []; if (done) parts.push(`${done} renamed`); if (conflicts) parts.push(`${conflicts} conflict(s)`); if (errors) parts.push(`${errors} error(s)`); renameStatus.textContent = parts.join(" · ") || "Nothing to rename."; renameAllBtn.disabled = false; _libraryIssuesDirty = done > 0; }); function _closeLibraryIssues() { closeModal("library-issues-modal"); if (_libraryIssuesDirty) { _libraryIssuesDirty = false; chrome.runtime.sendMessage({ type: "library_issues" }, (r) => { if (!r || !r.ok) return; const total = (r.bracket_names?.length || 0) + (r.nohyphen_names?.length || 0); document.getElementById("library-issues-results").textContent = total ? `${total} library issue(s) found. Review window is open.` : "No library issues found."; }); } } for (const id of ["library-issues-modal-close", "library-issues-modal-done"]) { document.getElementById(id).addEventListener("click", _closeLibraryIssues); } document.getElementById("library-issues-modal").addEventListener("click", (e) => { if (e.target.id === "library-issues-modal") _closeLibraryIssues(); }); (function () { const rebuildBtn = document.getElementById("cache-rebuild-run"); const rebuildMode = document.getElementById("cache-rebuild-mode"); const cacheStatusOut = document.getElementById("cache-status-results"); const scanJobOut = document.getElementById("scan-job-results"); let _optScanTimer = null; let _optScanning = false; const _stopOptPoll = () => { if (_optScanTimer) { clearInterval(_optScanTimer); _optScanTimer = null; } }; function _setOptScanningState(scanning) { _optScanning = scanning; rebuildBtn.textContent = scanning ? "✕ Cancel" : "Rebuild Cache"; if (rebuildMode) rebuildMode.disabled = scanning; rebuildBtn.style.background = scanning ? "#3a1a1a" : ""; rebuildBtn.style.borderColor = scanning ? "#722" : ""; rebuildBtn.style.color = scanning ? "#faa" : ""; } function _scanStatus(r) { if (!r || r.no_state) return "idle"; if (r.scanning && !r.done) return "running"; if (r.cancelled) return "cancelled"; if (r.scan_ok === false) return "failed"; if (r.done) return "completed"; return "idle"; } function _renderScanJob(r) { if (!r || r.no_state) { scanJobOut.innerHTML = `no scan job recorded yet`; return; } const status = _scanStatus(r); const pillCls = status === "completed" ? "ok" : status === "failed" ? "fail" : ""; const jobLabel = status === "running" ? "Current Scan Job" : "Last Scan Job"; const mode = r.scan_since ? `incremental ${r.scan_since}` : "full"; const scope = (r.scope && r.scope.length) ? r.scope.join(", ") : "configured scan roots"; const finished = r.finished_at || r.started_at || ""; const when = finished ? new Date(finished).toLocaleString() : ""; const elapsed = r.elapsed_s != null ? `${Number(r.elapsed_s).toFixed(1)}s` : ""; const count = r.file_count != null ? `${Number(r.file_count).toLocaleString()} files` : ""; const summary = [mode, scope, count, elapsed].filter(Boolean).join(" · "); const jobs = (r.remote_jobs && r.remote_jobs.length) ? r.remote_jobs : (r.remotes || []).map((remote, i) => ({ remote, status: remote === r.current_remote ? status : i < (r.current_index || 0) ? "completed" : "queued", files: remote === r.current_remote ? r.files_this_remote : null, total: remote === r.current_remote ? r.files_remote_total : null, })); const jobRoots = jobs.map((j) => j.remote).filter(Boolean); const retiredRoots = _configuredScanRoots.length ? jobRoots.filter((root) => !_configuredScanRoots.includes(root)) : []; const jobRows = jobs.map((j) => { const files = Number.isFinite(j.files) ? Number(j.files).toLocaleString() : "?"; const total = Number.isFinite(j.total) ? Number(j.total).toLocaleString() : ""; const pct = Number.isFinite(j.files) && Number.isFinite(j.total) && j.total > 0 ? Math.min(100, Math.round((j.files / j.total) * 100)) : null; const detail = [ j.label, j.incremental ? "incremental" : "", `${files}${total ? ` / ${total}` : ""} files`, Number.isFinite(j.skipped) && j.skipped ? `${j.skipped} skipped` : "", ].filter(Boolean).join(" · "); return `
${escapeHtml(j.remote || "?")} · ${escapeHtml(j.status || "queued")}
${escapeHtml(detail)}
${pct != null ? `
` : ""}
`; }).join(""); scanJobOut.innerHTML = `
${escapeHtml(jobLabel)}${when ? ` · ${escapeHtml(when)}` : ""}
${escapeHtml(status)}${escapeHtml(summary || "scan job")}
${retiredRoots.length ? `
Historical scan roots not in current config: ${escapeHtml(retiredRoots.join(", "))}. They are shown because this job was recorded before the scan roots changed.
` : ""} ${r.error ? `
${escapeHtml(r.error)}
` : ""} ${jobRows || `
waiting for remote progress...
`} `; } const _pollOptProgress = () => { chrome.runtime.sendMessage({ type: "scan-progress" }, (r) => { if (chrome.runtime.lastError || !r || !r.ok) return; _renderScanJob(r); if (r.done || !r.scanning) { _stopOptPoll(); _setOptScanningState(false); if (r.cancelled) { return; } else if (r.scan_ok !== false) { setTimeout(() => document.getElementById("cache-status-run").click(), 500); } return; } }); }; async function _refreshScanJob() { try { const cache = await chrome.runtime.sendMessage({ type: "cache-status" }); if (cache && cache.ok) rememberConfiguredScanRoots(cache); } catch {} _pollOptProgress(); } async function _startOptScan(scanRoots = [], forceSince = null) { const out = scanJobOut; if (_optScanning) { // Cancel in-progress scan rebuildBtn.disabled = true; rebuildBtn.textContent = "Cancelling…"; chrome.runtime.sendMessage({ type: "scan-cancel" }, () => { rebuildBtn.disabled = false; // State will update on next poll tick }); return; } // forceSince overrides dropdown (used by per-remote Refresh to stay incremental) const scanSince = forceSince !== null ? forceSince : (rebuildMode ? rebuildMode.value : ""); const scope = scanRoots.length ? `refresh ${scanRoots.join(", ")}` : "all configured scan roots"; const label = scanSince ? `incrementally update files changed in the last ${scanSince}` : "fully rebuild"; const button = scanRoots.length ? "Refresh" : "Rebuild"; if (!confirm(`${button} cache now?\n\nScope: ${scope}\nMode: ${label}\n\nThis can take several minutes.`)) return; cacheStatusOut.innerHTML = `starting scan…`; out.innerHTML = ""; try { const r = await chrome.runtime.sendMessage({ type: "run-scan", scanSince, scanRoots }); if (!r || !r.ok) { out.innerHTML = `scan failed: ${escapeHtml(r?.error || "no response")}`; return; } _setOptScanningState(true); _pollOptProgress(); _optScanTimer = setInterval(_pollOptProgress, 1500); } catch (err) { out.innerHTML = `scan failed: ${escapeHtml(err.message || String(err))}`; } } rebuildBtn.addEventListener("click", () => _startOptScan()); function _renderNonJavPanel(items, remote) { const panel = document.createElement("div"); panel.className = "nonjav-panel"; panel.dataset.remote = remote; const deleteEnabled = document.getElementById("enableDelete")?.checked; const delBtnHtml = deleteEnabled ? `` : `Enable deletion in settings to delete`; panel.innerHTML = `
${escapeHtml(remote)} · ${items.length} non-JAV file${items.length !== 1 ? "s" : ""} ${delBtnHtml}
${items.map(f => `
${escapeHtml(f.ext || "?")} ${escapeHtml(f.path)} ${deleteEnabled ? `` : ""}
`).join("")}
`; // Delete one panel.addEventListener("click", async (e) => { const btn = e.target.closest(".nonjav-del-one"); if (btn) { const item = btn.closest(".nonjav-item"); const path = item?.dataset.fullPath; if (!path) return; if (!confirm(`Delete?\n${path}`)) return; btn.disabled = true; const r = await chrome.runtime.sendMessage({ type: "delete-skipped", paths: [path] }); if (r?.ok) { item.classList.add("deleted"); item.querySelector(".nonjav-del-one")?.remove(); _updateNonJavDelAll(panel); } else { btn.disabled = false; panel.querySelector(".nonjav-status").textContent = "Error: " + (r?.error || "failed"); } return; } const delAll = e.target.closest(".nonjav-del-all"); if (delAll) { const allItems = [...panel.querySelectorAll(".nonjav-item:not(.deleted)")]; const paths = allItems.map(i => i.dataset.fullPath).filter(Boolean); if (!paths.length) return; if (!confirm(`Delete all ${paths.length} non-JAV file(s) from ${remote}?`)) return; delAll.disabled = true; const statusEl = panel.querySelector(".nonjav-status"); statusEl.textContent = `Deleting ${paths.length} file(s)…`; const r = await chrome.runtime.sendMessage({ type: "delete-skipped", paths }); const ok = r?.deleted_count || 0; const fail = r?.failed_count || 0; if (ok) { // Mark successfully deleted items const deletedPaths = new Set( (r.results || []).filter(x => x.ok).map(x => x.path) ); allItems.forEach(i => { if (deletedPaths.has(i.dataset.fullPath)) { i.classList.add("deleted"); i.querySelector(".nonjav-del-one")?.remove(); } }); _updateNonJavDelAll(panel); } statusEl.textContent = fail ? `Deleted ${ok}, failed ${fail}. Check deletion settings.` : `Deleted ${ok} file(s).`; } }); return panel; } function _updateNonJavDelAll(panel) { const remaining = panel.querySelectorAll(".nonjav-item:not(.deleted)").length; const btn = panel.querySelector(".nonjav-del-all"); if (btn) { btn.textContent = `Delete All (${remaining})`; btn.disabled = remaining === 0; } } cacheStatusOut.addEventListener("click", (event) => { const showSkipped = event.target.closest(".cache-show-skipped"); if (showSkipped) { const remote = showSkipped.dataset.remote; // Toggle: if panel already open, close it const existing = cacheStatusOut.querySelector(`.nonjav-panel[data-remote="${CSS.escape(remote)}"]`); if (existing) { existing.remove(); showSkipped.textContent = showSkipped.textContent.replace("▴", "▾"); return; } showSkipped.textContent = showSkipped.textContent.replace("▾", "▴"); // Find skipped items from last cache status result const items = (_cacheSkippedByRemote?.get(remote)) || []; const panel = _renderNonJavPanel(items, remote); // Insert after the row containing this button showSkipped.closest("div")?.after(panel); return; } const refresh = event.target.closest(".cache-refresh-remote"); if (refresh) { const remote = refresh.dataset.remote || ""; if (!remote) return; // Per-remote Refresh is always incremental — inherit dropdown value if it's a // duration (not "Full Rebuild"), otherwise default to 24h. const dropdownVal = rebuildMode ? rebuildMode.value : ""; const refreshSince = dropdownVal || "24h"; _startOptScan([remote], refreshSince); return; } }); document.getElementById("scan-job-clear").addEventListener("click", async () => { if (!confirm("Clear recorded scan job history?\n\nThis only clears the Scan Job panel state. It does not change cache.json.")) return; scanJobOut.textContent = "clearing scan job history..."; const r = await chrome.runtime.sendMessage({ type: "scan-clear" }); if (!r || !r.ok) { scanJobOut.innerHTML = `clear failed: ${escapeHtml(r?.error || "no response")}`; return; } _renderScanJob({ ok: true, no_state: true }); }); // If Options is opened while a scan is already running, attach to it instead // of showing an idle Rebuild button. _refreshScanJob(); chrome.runtime.sendMessage({ type: "scan-progress" }, (r) => { if (chrome.runtime.lastError || !r || !r.ok) return; _renderScanJob(r); if (!r.scanning) return; _setOptScanningState(true); _optScanTimer = setInterval(_pollOptProgress, 1500); }); })();