Step 6b: extract Library Issues from options.js
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 <noreply@anthropic.com>
This commit is contained in:
@@ -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.
|
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.
|
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.
|
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.)
|
(Step 4 in the plan is a paired-extraction sub-task of step 6; folded into step 6 ship.)
|
||||||
|
|
||||||
**Pending (in execution order):**
|
**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 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 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.
|
- **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.
|
||||||
|
|||||||
@@ -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 = `<div class="li-empty" style="color:#f87171;">Error: ${escapeHtml(r?.error || "no response")}</div>`;
|
||||||
|
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(`<div class="li-empty">✓ No library issues found. All filenames are canonical.</div>`);
|
||||||
|
} else {
|
||||||
|
parts.push(`<div class="li-stats"><b>${total}</b> file${total !== 1 ? "s" : ""} with non-canonical names — <b>${brackets.length}</b> bracket-wrapped, <b>${nohyphens.length}</b> no-hyphen</div>`);
|
||||||
|
|
||||||
|
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 `<div class="li-row" data-remote="${escapeHtml(entry.remote)}" data-old="${escapeHtml(entry.path)}" data-new="${escapeHtml(dir + entry.canonical_name)}">
|
||||||
|
<span class="li-tag ${tagClass}">${tagLabel}</span>
|
||||||
|
<div class="li-names">
|
||||||
|
<span class="li-old" title="${escapeHtml(entry.path)}">${escapeHtml(fname)}</span>
|
||||||
|
<span class="li-new" title="${escapeHtml(entry.canonical_name)}">→ ${escapeHtml(entry.canonical_name)}</span>
|
||||||
|
</div>
|
||||||
|
<span class="li-sz">${escapeHtml(entry.size_human || "")}</span>
|
||||||
|
<button class="li-rename-btn" type="button">Rename</button>
|
||||||
|
</div>`;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (brackets.length) {
|
||||||
|
parts.push(`<div class="li-section-head">Bracket-wrapped IDs (${brackets.length})</div>`);
|
||||||
|
parts.push(brackets.map((e) => makeRow(e, "bracket", "[ ]")).join(""));
|
||||||
|
}
|
||||||
|
if (nohyphens.length) {
|
||||||
|
parts.push(`<div class="li-section-head">No-hyphen IDs (${nohyphens.length})</div>`);
|
||||||
|
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 = `<div class="li-stats">Loading library issues from cache…</div>`;
|
||||||
|
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 = `<span style="color:#777;">no scan job recorded yet</span>`;
|
||||||
|
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 `<div class="scan-remote">
|
||||||
|
<div><span style="color:#9dccff;">${escapeHtml(j.remote || "?")}</span> · <span>${escapeHtml(j.status || "queued")}</span></div>
|
||||||
|
<div style="color:#777;">${escapeHtml(detail)}</div>
|
||||||
|
${pct != null ? `<div class="scan-track"><div class="scan-fill" style="width:${pct}%"></div></div>` : ""}
|
||||||
|
</div>`;
|
||||||
|
}).join("");
|
||||||
|
scanJobOut.innerHTML = `
|
||||||
|
<div style="color:#888;margin-bottom:6px;">${escapeHtml(jobLabel)}${when ? ` · ${escapeHtml(when)}` : ""}</div>
|
||||||
|
<div class="scan-job-head"><span class="scan-pill ${pillCls}">${escapeHtml(status)}</span><span>${escapeHtml(summary || "scan job")}</span></div>
|
||||||
|
${retiredRoots.length ? `<div class="section-note warn" style="margin:0 0 8px;">Historical scan roots not in current config: ${escapeHtml(retiredRoots.join(", "))}. They are shown because this job was recorded before the scan roots changed.</div>` : ""}
|
||||||
|
${r.error ? `<div style="color:#faa;margin-bottom:6px;">${escapeHtml(r.error)}</div>` : ""}
|
||||||
|
${jobRows || `<div style="color:#777;">waiting for remote progress...</div>`}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 = `<span style="color:#6ec1ff;">starting scan…</span>`;
|
||||||
|
out.innerHTML = "";
|
||||||
|
try {
|
||||||
|
const r = await chrome.runtime.sendMessage({ type: "run-scan", scanSince, scanRoots });
|
||||||
|
if (!r || !r.ok) {
|
||||||
|
out.innerHTML = `<span style="color:#faa;">scan failed:</span> ${escapeHtml(r?.error || "no response")}`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_setOptScanningState(true);
|
||||||
|
_pollOptProgress();
|
||||||
|
_optScanTimer = setInterval(_pollOptProgress, 1500);
|
||||||
|
} catch (err) {
|
||||||
|
out.innerHTML = `<span style="color:#faa;">scan failed:</span> ${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
|
||||||
|
? `<button class="nonjav-del-all" type="button" title="Delete all non-JAV files in this remote">Delete All (${items.length})</button>`
|
||||||
|
: `<span style="font-size:11px;color:#555;">Enable deletion in settings to delete</span>`;
|
||||||
|
panel.innerHTML = `
|
||||||
|
<div class="nonjav-panel-head">
|
||||||
|
<span class="nonjav-panel-title">${escapeHtml(remote)} · ${items.length} non-JAV file${items.length !== 1 ? "s" : ""}</span>
|
||||||
|
${delBtnHtml}
|
||||||
|
</div>
|
||||||
|
<div class="nonjav-list">${items.map(f => `
|
||||||
|
<div class="nonjav-item" data-full-path="${escapeHtml(f.full_path)}">
|
||||||
|
<span class="nonjav-ext">${escapeHtml(f.ext || "?")}</span>
|
||||||
|
<span class="nonjav-path" title="${escapeHtml(f.full_path)}">${escapeHtml(f.path)}</span>
|
||||||
|
${deleteEnabled ? `<button class="nonjav-del-one" type="button">Delete</button>` : ""}
|
||||||
|
</div>`).join("")}
|
||||||
|
</div>
|
||||||
|
<div class="nonjav-status"></div>`;
|
||||||
|
// 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 = `<span style="color:#faa;">clear failed:</span> ${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);
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
|
||||||
@@ -803,6 +803,7 @@
|
|||||||
|
|
||||||
<script src="options-cache.js"></script>
|
<script src="options-cache.js"></script>
|
||||||
<script src="options-dupe-review.js"></script>
|
<script src="options-dupe-review.js"></script>
|
||||||
|
<script src="options-library-issues.js"></script>
|
||||||
<script src="options.js"></script>
|
<script src="options.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
-453
@@ -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 = `<div class="li-empty" style="color:#f87171;">Error: ${escapeHtml(r?.error || "no response")}</div>`;
|
|
||||||
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(`<div class="li-empty">✓ No library issues found. All filenames are canonical.</div>`);
|
|
||||||
} else {
|
|
||||||
parts.push(`<div class="li-stats"><b>${total}</b> file${total !== 1 ? "s" : ""} with non-canonical names — <b>${brackets.length}</b> bracket-wrapped, <b>${nohyphens.length}</b> no-hyphen</div>`);
|
|
||||||
|
|
||||||
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 `<div class="li-row" data-remote="${escapeHtml(entry.remote)}" data-old="${escapeHtml(entry.path)}" data-new="${escapeHtml(dir + entry.canonical_name)}">
|
|
||||||
<span class="li-tag ${tagClass}">${tagLabel}</span>
|
|
||||||
<div class="li-names">
|
|
||||||
<span class="li-old" title="${escapeHtml(entry.path)}">${escapeHtml(fname)}</span>
|
|
||||||
<span class="li-new" title="${escapeHtml(entry.canonical_name)}">→ ${escapeHtml(entry.canonical_name)}</span>
|
|
||||||
</div>
|
|
||||||
<span class="li-sz">${escapeHtml(entry.size_human || "")}</span>
|
|
||||||
<button class="li-rename-btn" type="button">Rename</button>
|
|
||||||
</div>`;
|
|
||||||
};
|
|
||||||
|
|
||||||
if (brackets.length) {
|
|
||||||
parts.push(`<div class="li-section-head">Bracket-wrapped IDs (${brackets.length})</div>`);
|
|
||||||
parts.push(brackets.map((e) => makeRow(e, "bracket", "[ ]")).join(""));
|
|
||||||
}
|
|
||||||
if (nohyphens.length) {
|
|
||||||
parts.push(`<div class="li-section-head">No-hyphen IDs (${nohyphens.length})</div>`);
|
|
||||||
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 = `<div class="li-stats">Loading library issues from cache…</div>`;
|
|
||||||
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 = `<span style="color:#777;">no scan job recorded yet</span>`;
|
|
||||||
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 `<div class="scan-remote">
|
|
||||||
<div><span style="color:#9dccff;">${escapeHtml(j.remote || "?")}</span> · <span>${escapeHtml(j.status || "queued")}</span></div>
|
|
||||||
<div style="color:#777;">${escapeHtml(detail)}</div>
|
|
||||||
${pct != null ? `<div class="scan-track"><div class="scan-fill" style="width:${pct}%"></div></div>` : ""}
|
|
||||||
</div>`;
|
|
||||||
}).join("");
|
|
||||||
scanJobOut.innerHTML = `
|
|
||||||
<div style="color:#888;margin-bottom:6px;">${escapeHtml(jobLabel)}${when ? ` · ${escapeHtml(when)}` : ""}</div>
|
|
||||||
<div class="scan-job-head"><span class="scan-pill ${pillCls}">${escapeHtml(status)}</span><span>${escapeHtml(summary || "scan job")}</span></div>
|
|
||||||
${retiredRoots.length ? `<div class="section-note warn" style="margin:0 0 8px;">Historical scan roots not in current config: ${escapeHtml(retiredRoots.join(", "))}. They are shown because this job was recorded before the scan roots changed.</div>` : ""}
|
|
||||||
${r.error ? `<div style="color:#faa;margin-bottom:6px;">${escapeHtml(r.error)}</div>` : ""}
|
|
||||||
${jobRows || `<div style="color:#777;">waiting for remote progress...</div>`}
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
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 = `<span style="color:#6ec1ff;">starting scan…</span>`;
|
|
||||||
out.innerHTML = "";
|
|
||||||
try {
|
|
||||||
const r = await chrome.runtime.sendMessage({ type: "run-scan", scanSince, scanRoots });
|
|
||||||
if (!r || !r.ok) {
|
|
||||||
out.innerHTML = `<span style="color:#faa;">scan failed:</span> ${escapeHtml(r?.error || "no response")}`;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
_setOptScanningState(true);
|
|
||||||
_pollOptProgress();
|
|
||||||
_optScanTimer = setInterval(_pollOptProgress, 1500);
|
|
||||||
} catch (err) {
|
|
||||||
out.innerHTML = `<span style="color:#faa;">scan failed:</span> ${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
|
|
||||||
? `<button class="nonjav-del-all" type="button" title="Delete all non-JAV files in this remote">Delete All (${items.length})</button>`
|
|
||||||
: `<span style="font-size:11px;color:#555;">Enable deletion in settings to delete</span>`;
|
|
||||||
panel.innerHTML = `
|
|
||||||
<div class="nonjav-panel-head">
|
|
||||||
<span class="nonjav-panel-title">${escapeHtml(remote)} · ${items.length} non-JAV file${items.length !== 1 ? "s" : ""}</span>
|
|
||||||
${delBtnHtml}
|
|
||||||
</div>
|
|
||||||
<div class="nonjav-list">${items.map(f => `
|
|
||||||
<div class="nonjav-item" data-full-path="${escapeHtml(f.full_path)}">
|
|
||||||
<span class="nonjav-ext">${escapeHtml(f.ext || "?")}</span>
|
|
||||||
<span class="nonjav-path" title="${escapeHtml(f.full_path)}">${escapeHtml(f.path)}</span>
|
|
||||||
${deleteEnabled ? `<button class="nonjav-del-one" type="button">Delete</button>` : ""}
|
|
||||||
</div>`).join("")}
|
|
||||||
</div>
|
|
||||||
<div class="nonjav-status"></div>`;
|
|
||||||
// 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 = `<span style="color:#faa;">clear failed:</span> ${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 ----------
|
// ---------- adapters ----------
|
||||||
|
|
||||||
function renderAdapters(list) {
|
function renderAdapters(list) {
|
||||||
|
|||||||
Reference in New Issue
Block a user