From e4ee06b19f0da35e834c3cf8c3142f794771af7f Mon Sep 17 00:00:00 2001 From: admin Date: Fri, 22 May 2026 21:21:58 +0200 Subject: [PATCH] Step 6b: extract Library Issues from options.js MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Continues the options.js split. New file: options-library-issues.js 453 lines After this step: options-cache.js 161 lines options-dupe-review.js 616 lines options-library-issues.js 453 lines options.js 1903 lines (was 2356 after step 6) Library Issues block was fully self-contained (lastLibraryIssues, _libraryIssuesDirty, renderLibraryIssues, _closeLibraryIssues, and the bottom IIFE wrapping _optScanTimer / _setOptScanningState / _pollOptProgress for optimization-scan progress polling). No external callers of its identifiers. Reads _configuredScanRoots / _cacheSkippedByRemote and calls rememberConfiguredScanRoots from options-cache.js by bare reference — same cross-file binding pattern proven in step 6. node --check passes on each file and on the concatenation of all four files in load order. Concat = 3133 lines, matching pre-split total. Co-Authored-By: Claude Opus 4.7 --- AGENTS.md | 3 +- options-library-issues.js | 453 ++++++++++++++++++++++++++++++++++++++ options.html | 1 + options.js | 453 -------------------------------------- 4 files changed, 456 insertions(+), 454 deletions(-) create mode 100644 options-library-issues.js diff --git a/AGENTS.md b/AGENTS.md index a84db65..457b4ee 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -136,12 +136,13 @@ Done in rc-jav catalog loading. Catalog CSV/XML paths are normalized from Window 3. **Transfer Assistant wizard deleted.** "Setup & Transfer" pane renamed to "Setup". Replacement: Extension ID display + Copy button added to Diagnostics → Native host registration fieldset (always visible, not failure-gated). Sidebar entry, fieldset, modal, and ~107 lines of JS removed. 5. **Recent Activity + Search Troubleshooting moved to new Debug Tools pane.** Verified Recent Activity is search-trigger-only by reading `background.js` — `recordActivity()` is NOT called from `delete-file` handler. No audit-value split needed. New sidebar entry "Debug Tools" under System group; new `pane-debug` houses both fieldsets. 6. **options.js split — Cache & Scans + Duplicate Review paired extraction.** `options.js` 3133 → 2356 lines. New files: `options-cache.js` (161 lines, Cache & Scans block), `options-dupe-review.js` (616 lines, Dup Review + Keep Ranking incl. bottom `loadKeepRanking()` call). Script-tag order in `options.html`: cache → dupe-review → options.js (body bottom). Cross-script binding visibility (vanilla classic scripts share global declarative env): Library Issues code still in options.js reads `_configuredScanRoots` / `_cacheSkippedByRemote` / calls `rememberConfiguredScanRoots` from cache file by bare reference. Calls to `escapeHtml` / `openModal` / `closeModal` / `keepActionViewport` / `clearNativeRepairCard` / `renderNativeMessagingFailure` from extracted files all occur inside event handlers (resolved at call time, after options.js parses). Repo `git init`'d before this step; baseline commit `f8e781f` is the rollback point. Verified by `node --check` on each file and on concatenated script. +6b. **options.js split — Library Issues extraction.** `options.js` 2356 → 1903 lines. New file: `options-library-issues.js` (453 lines) — covers `lastLibraryIssues`, `_libraryIssuesDirty`, `renderLibraryIssues`, `_closeLibraryIssues`, and the bottom IIFE that wraps `_optScanTimer` / `_setOptScanningState` / `_pollOptProgress` for optimization-scan progress polling. Block was fully self-contained (no external callers of its identifiers). Reads `_configuredScanRoots` / `_cacheSkippedByRemote` / calls `rememberConfiguredScanRoots` from `options-cache.js` — same cross-file binding pattern proven in step 6. Script-tag order in `options.html`: cache → dupe-review → library-issues → options.js. `node --check` passes on each file and on concatenation; line count of concat (3133) matches pre-split total exactly. (Step 4 in the plan is a paired-extraction sub-task of step 6; folded into step 6 ship.) **Pending (in execution order):** -- **Step 6b — continue options.js split with Library Issues, Debug Tools handlers, Settings sub-tabs.** Library Issues is the next obvious ~450-line block (lines 1505–1957 in pre-split numbering, now in options.js mid-section). Reads `_configuredScanRoots` and `_cacheSkippedByRemote` from `options-cache.js` — cross-file binding already exercised, so the extraction is lower risk than the first pair. +- **Step 6c — finish options.js split (optional).** Remaining options.js (1903 lines) still holds: settings load/save, backup/restore, recent activity, search test bench, bulk ID check, adapters, ID normalizers, part detectors, element picker, overlay previews, diagnostics, profiles, paths, and the bottom-entry IIFE. Candidates for extraction: Diagnostics (~250 lines, 1354–1603 in current options.js), Profiles (~265 lines), Adapters + ID normalizers + Part detectors as a "rules editors" file (~330 lines combined). Diminishing returns past this point — bottom IIFE + load/save core should stay in `options.js` as the entry point. - **Step 7a — Bulk Check standalone window.** New `bulk-check.html` opened as detached `chrome.windows.create({ type: 'popup', width: 640, height: 540 })` from a "Bulk Check" launcher button in the popup. Single canonical entry path — NOT a Console sidebar tab. Window dedup via `chrome.storage.session`, last-paste persisted via `chrome.storage.local`. - **Step 8 — Shared fixture corpus.** Top-level `D:\DEV\Project\rclone-jav\fixtures\` (neutral location, NOT inside Python or extension repo). JSON cases for query-ID extraction (extension), filename ID extraction (Python), shared normalization. - **Step 9 — Cache contract design.** CACHE_VERSION already exists (currently 3). Add ID_RULES_VERSION concept: schema bump = force rebuild, rules bump = warn-and-mark-stale. diff --git a/options-library-issues.js b/options-library-issues.js new file mode 100644 index 0000000..114de71 --- /dev/null +++ b/options-library-issues.js @@ -0,0 +1,453 @@ +// ---- 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); + }); +})(); + diff --git a/options.html b/options.html index ae0cdc9..045b971 100644 --- a/options.html +++ b/options.html @@ -803,6 +803,7 @@ + diff --git a/options.js b/options.js index 1a586ed..6fb6d96 100644 --- a/options.js +++ b/options.js @@ -725,459 +725,6 @@ document.getElementById("bulk-id-clear").addEventListener("click", () => { }); -// ---- 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); - }); -})(); - // ---------- adapters ---------- function renderAdapters(list) {