From da09434419d9d32cb536aa8ccd832b2856de6585 Mon Sep 17 00:00:00 2001 From: admin Date: Fri, 22 May 2026 21:10:04 +0200 Subject: [PATCH] Step 6: extract Cache & Scans + Duplicate Review from options.js Splits the 3133-line options.js into three vanilla scripts loaded in order at the bottom of options.html: options-cache.js 161 lines (Cache & Scans block) options-dupe-review.js 616 lines (Duplicate Review + Keep Ranking) options.js 2356 lines (everything else) No behavior change. Cross-file references work because classic + diff --git a/options.js b/options.js index cde48b0..1a586ed 100644 --- a/options.js +++ b/options.js @@ -494,167 +494,6 @@ document.getElementById("add-current-site").addEventListener("click", async () = } }); -// ---------- cache status ---------- - -function fmtCacheAge(hours) { - if (!Number.isFinite(hours)) return "?"; - if (hours < 1) return `${Math.round(hours * 60)}m`; - if (hours < 24) return `${hours.toFixed(1)}h`; - return `${(hours / 24).toFixed(1)}d`; -} - -let _configuredScanRoots = []; -let _cacheSkippedByRemote = new Map(); -let _skippedModalText = ""; - -function rememberConfiguredScanRoots(r) { - _configuredScanRoots = [ - ...(r?.configured?.default_source || []), - ...(r?.configured?.default_target || []), - ]; -} - -function setupHealthRow(level, name, detail) { - const icon = level === "ok" ? "✓" : level === "warn" ? "!" : level === "fail" ? "✗" : "i"; - return `
${icon}${escapeHtml(name)}${escapeHtml(detail)}
`; -} - -function openSkippedModal(remote) { - const items = _cacheSkippedByRemote.get(remote) || []; - const summary = document.getElementById("skipped-modal-summary"); - const list = document.getElementById("skipped-modal-list"); - document.getElementById("skipped-modal-subtitle").textContent = `${remote} · ${items.length} skipped`; - const reasonCounts = new Map(); - for (const item of items) reasonCounts.set(item.reason || "unparsed ID", (reasonCounts.get(item.reason || "unparsed ID") || 0) + 1); - summary.innerHTML = [...reasonCounts.entries()] - .sort((a, b) => b[1] - a[1]) - .map(([reason, count]) => `${escapeHtml(count)} ${escapeHtml(reason)}`) - .join(""); - list.innerHTML = items.map((item) => `
-
${escapeHtml(item.name || item.path || "?")}
-
${escapeHtml(item.reason || "unparsed ID")}
-
${escapeHtml(item.path || "")}
-
`).join("") || `
No skipped IDs recorded for this remote.
`; - _skippedModalText = [ - `Skipped IDs for ${remote}`, - ...items.map((item) => `${item.name || item.path || "?"}\t${item.reason || "unparsed ID"}\t${item.path || ""}`), - ].join("\n"); - openModal("skipped-modal"); -} - -function closeSkippedModal() { - closeModal("skipped-modal"); -} - -document.getElementById("skipped-modal-close").addEventListener("click", closeSkippedModal); -document.getElementById("skipped-modal-done").addEventListener("click", closeSkippedModal); -document.getElementById("skipped-modal").addEventListener("click", (event) => { - if (event.target.id === "skipped-modal") closeSkippedModal(); -}); -document.getElementById("skipped-modal-copy").addEventListener("click", async () => { - if (!_skippedModalText) return; - await navigator.clipboard.writeText(_skippedModalText); - const btn = document.getElementById("skipped-modal-copy"); - btn.textContent = "Copied"; - setTimeout(() => { btn.textContent = "Copy List"; }, 1200); -}); - -document.getElementById("setup-health-run").addEventListener("click", (event) => - keepActionViewport(event.currentTarget, async () => { - const out = document.getElementById("setup-health-results"); - clearNativeRepairCard(); - out.textContent = "checking setup health..."; - const [settings, cache, host] = await Promise.all([ - chrome.runtime.sendMessage({ type: "get-settings" }), - chrome.runtime.sendMessage({ type: "cache-status" }), - chrome.runtime.sendMessage({ type: "host-status" }), - ]); - const rows = []; - const mode = settings?.quickMode !== false ? "LIVE" : "CACHE"; - rows.push(setupHealthRow(settings?.scanPaused ? "warn" : "ok", "Search state", - settings?.scanPaused ? `${mode} mode · scanning paused` : `${mode} mode · scanning enabled`)); - rows.push(setupHealthRow("info", "Library profile", - settings?.activeProfile || "config.json defaults")); - const nativeBlocked = [cache, host].find((r) => r && !r.ok && r.error_kind); - if (nativeBlocked) await renderNativeMessagingFailure(nativeBlocked); - if (!cache?.ok && cache?.error_kind) { - rows.push(setupHealthRow("warn", "Cache", "Blocked until native host registration is fixed.")); - } else if (!cache?.ok) { - rows.push(setupHealthRow("fail", "Cache", cache?.error || "cache status unavailable")); - } else if (!cache.cache_exists) { - rows.push(setupHealthRow("warn", "Cache", "cache.json missing; cached searches need a rebuild")); - } else { - const remotes = cache.remotes || []; - const stale = remotes.filter((r) => r.stale || r.status === "never_scanned"); - const files = remotes.reduce((sum, r) => sum + Number(r.file_count || 0), 0); - rows.push(setupHealthRow(stale.length || (cache.warnings || []).length ? "warn" : "ok", "Cache", - `${files.toLocaleString()} files · ${remotes.length} remote(s) · ${stale.length} stale/unscanned`)); - } - if (!host?.ok && host?.error_kind) { - rows.push(setupHealthRow("warn", "Native host", "Registration is required before host checks can run.")); - } else if (!host?.ok) { - rows.push(setupHealthRow("fail", "Native host", host?.error || "host status unavailable")); - } else { - const failed = (host.checks || []).filter((c) => c.status === "fail"); - rows.push(setupHealthRow(failed.length ? "fail" : "ok", "Native host", - failed.length ? `${failed.length} registration check(s) failed; use Diagnostics` : "registration checks passed")); - } - out.innerHTML = rows.join(""); - }) -); - -document.getElementById("cache-status-run").addEventListener("click", async () => { - const out = document.getElementById("cache-status-results"); - out.textContent = "checking cache..."; - try { - const r = await chrome.runtime.sendMessage({ type: "cache-status" }); - if (!r || !r.ok) { - out.innerHTML = `error: ${escapeHtml(r?.error || "no response")}`; - return; - } - rememberConfiguredScanRoots(r); - _cacheSkippedByRemote = new Map((r.remotes || []).map((m) => [m.remote, m.skipped_items || []])); - if (!r.cache_exists) { - const configured = (r.remotes || []).map((m) => - `
! ${escapeHtml(m.remote)} · never scanned
` - ); - out.innerHTML = [ - `
cache not found
`, - `
${escapeHtml(r.cache_path || "")}
`, - ...configured, - ].join(""); - return; - } - const rows = [ - `
Path: ${escapeHtml(r.cache_path || "")}
`, - `
Version: ${escapeHtml(r.version ?? "?")}
`, - `
Stale after: ${escapeHtml(r.stale_hours ?? 24)}h
`, - `
Configured target: ${escapeHtml((r.configured?.default_target || []).join(", ") || "(none)")}
`, - `
Configured source: ${escapeHtml((r.configured?.default_source || []).join(", ") || "(none)")}
`, - ]; - for (const m of r.remotes || []) { - const color = m.status === "never_scanned" || m.stale ? "#ffa" : "#afa"; - const state = m.status === "never_scanned" ? "never scanned" : `${m.status || (m.stale ? "stale" : "fresh")} · age ${fmtCacheAge(m.age_hours)}`; - const skippedCount = Number(m.skipped_count) || 0; - const skippedNote = skippedCount - ? ` · ` - : ""; - rows.push(`
${escapeHtml(m.remote)} · ${escapeHtml(state)} · ${escapeHtml(m.file_count)} files${skippedNote}
`); - for (const issue of m.issues || []) { - rows.push(`
! ${escapeHtml(issue.count)} ${escapeHtml(issue.message)}
`); - } - } - if ((r.warnings || []).length) { - rows.push(`
Rebuild cache recommended:
`); - for (const w of r.warnings || []) { - rows.push(`
! ${escapeHtml(w.message || w.code)}
`); - } - } - out.innerHTML = rows.join(""); - } catch (err) { - out.innerHTML = `error: ${escapeHtml(err.message || String(err))}`; - } -}); // ---------- recent activity ---------- @@ -885,622 +724,6 @@ document.getElementById("bulk-id-clear").addEventListener("click", () => { document.getElementById("bulk-id-results").innerHTML = ""; }); -// ---------- duplicate review ---------- - -let lastDupeReview = null; - -function dupePath(row) { - return row?.full_path || row?.path || row?.jav_id || "?"; -} - -function _groupFmtKey(keep, deletions) { - const all = [keep, ...deletions]; - const exts = new Set(all.map(f => (f.path || f.full_path || "").split(".").pop().toLowerCase()).filter(e => e && e.length <= 4 && /^[a-z]+$/.test(e))); - if (exts.has("mkv") && exts.has("mp4") && !exts.has("wmv") && !exts.has("avi")) return "MKV/MP4"; - if (exts.has("wmv") && exts.has("mp4") && !exts.has("mkv")) return "WMV/MP4"; - if (exts.has("avi") && exts.has("mp4") && !exts.has("mkv")) return "AVI/MP4"; - if (exts.size === 1) return "Same format"; - return null; // mixed/unusual — visible under All, no chip -} - -function _pathRes(path) { - if (/\[2160p\]/i.test(path) || /\b4[kK]\b/.test(path)) return 2160; - if (/\[1080p\]/i.test(path)) return 1080; - if (/\[720p\]/i.test(path)) return 720; - if (/\[480p\]/i.test(path)) return 480; - return 0; -} - -function _groupResKey(keep, deletions) { - const keepRes = _pathRes(dupePath(keep)); - const maxDelRes = deletions.reduce((m, d) => Math.max(m, _pathRes(dupePath(d))), 0); - if (keepRes === 0 && maxDelRes === 0) return "unknown"; - if (keepRes === maxDelRes) return "same"; - return keepRes > maxDelRes ? "upgrade" : "downgrade"; -} - -let _drActiveFmt = "all"; -let _drActiveRes = "all"; -let _drActiveStatus = "all"; -let _drActiveParts = "all"; -let _drActiveVip = "all"; -let _drActiveSearch = ""; - -function _drPromoteToKeep(row) { - row.classList.remove("del", "confirmed", "unconfirmed", "queued"); - row.classList.add("keep"); - const tag = row.querySelector(".dr-tag"); - tag.textContent = "KEEP"; - tag.className = "dr-tag keep"; -} - -function _drDemoteToDelete(row) { - row.classList.remove("keep", "queued"); - row.classList.add("del", "confirmed"); - const tag = row.querySelector(".dr-tag"); - tag.textContent = "DELETE?"; - tag.className = "dr-tag del"; -} - -function _drApplyFilters() { - const wraps = document.querySelectorAll("#dupe-review-modal-body .dr-card-wrap"); - for (const wrap of wraps) { - const fmtMatch = _drActiveFmt === "all" || wrap.dataset.fmt === _drActiveFmt; - const resMatch = _drActiveRes === "all" || wrap.dataset.res === _drActiveRes; - let statusMatch = true; - if (_drActiveStatus !== "all") { - const skipped = wrap.classList.contains("skipped"); - const delRows = wrap.querySelectorAll(".dr-row.del"); - const doneRows = wrap.querySelectorAll(".dr-row.del.done"); - const allDone = delRows.length > 0 && doneRows.length === delRows.length; - if (_drActiveStatus === "skipped") statusMatch = skipped; - else if (_drActiveStatus === "done") statusMatch = !skipped && allDone; - else statusMatch = !skipped && !allDone; // pending - } - const partsMatch = _drActiveParts === "all" || wrap.dataset.parts === "1"; - const vipMatch = _drActiveVip === "all" || wrap.dataset.vip === "1"; - const q = _drActiveSearch; - const searchMatch = !q - || (wrap.querySelector(".dr-card-id")?.textContent.toLowerCase().includes(q)) - || ([...wrap.querySelectorAll(".dr-path")].some(p => p.textContent.toLowerCase().includes(q))); - wrap.classList.toggle("dr-hidden", !(fmtMatch && resMatch && statusMatch && partsMatch && vipMatch && searchMatch)); - } -} - -function _drBadges(keep, deletions, catalogs) { - const all = [keep, ...deletions, ...catalogs]; - const paths = all.map((f) => dupePath(f)); - const out = []; - if (paths.some((p) => /\[2160p\]/i.test(p) || /\b4k\b/i.test(p))) { - out.push(`4K`); - } else if (paths.some((p) => /\[1080p\]/i.test(p))) { - out.push(`1080p`); - } - if (paths.some((p) => /clearjav/i.test(p))) { - out.push(`CLEARJAV`); - } - const exts = new Set( - all.map((f) => (f.path || f.full_path || "").split(".").pop().toLowerCase()).filter((e) => e && e.length <= 4) - ); - if (exts.size > 1) { - out.push(`${escapeHtml([...exts].join("/").toUpperCase())}`); - } else if (exts.has("mkv")) { - out.push(`MKV`); - } - return out.join(""); -} - -function renderDupeReview(r) { - const out = document.getElementById("dupe-review-modal-body"); - const summary = document.getElementById("dupe-review-results"); - const exportBtn = document.getElementById("dupe-review-export"); - if (!r || !r.ok) { - lastDupeReview = null; - exportBtn.disabled = true; - out.innerHTML = `
Error: ${escapeHtml(r?.error || "no response")}
`; - summary.innerHTML = out.innerHTML; - openModal("dupe-review-modal"); - return; - } - lastDupeReview = r; - exportBtn.disabled = false; - _drActiveFmt = "all"; - _drActiveRes = "all"; - _drActiveStatus = "all"; - _drActiveParts = "all"; - _drActiveVip = "all"; - _drActiveSearch = ""; - - const groups = Object.entries(r.groups || {}); - const totalCandidates = groups.reduce((n, [, g]) => n + (g.delete_candidates?.length || 0), 0); - const roots = [ - ...(r.roots?.source || []).map((root) => `source: ${root}`), - ...(r.roots?.target || []).map((root) => `target: ${root}`), - ]; - - // Compute per-group fmt/res keys and counts for filter bar - const fmtCounts = {}; - const resCounts = {}; - let partsCount = 0; - let vipCount = 0; - let riskCount = 0; - for (const [javId, g] of groups) { - const fk = _groupFmtKey(g.keep || {}, g.delete_candidates || []); - const rk = _groupResKey(g.keep || {}, g.delete_candidates || []); - fmtCounts[fk] = (fmtCounts[fk] || 0) + 1; - resCounts[rk] = (resCounts[rk] || 0) + 1; - if (javId.includes("#part")) partsCount++; - if ([g.keep, ...(g.delete_candidates || [])].some((row) => /(?:^|[\\/])clearjav(?:[\\/]|$)/i.test(dupePath(row)))) vipCount++; - if ((g.risks || []).length) riskCount++; - } - - const parts = []; - - // Filter bar (sticky top) - if (groups.length) { - const fmtOrder = ["MKV/MP4", "WMV/MP4", "AVI/MP4", "Same format"]; - const resOrder = [ - { key: "same", label: "Same res" }, - { key: "upgrade", label: "Upgrade" }, - ]; - const fmtChips = fmtOrder - .filter(k => fmtCounts[k]) - .map(k => ``) - .join(""); - const resChips = resOrder - .filter(({ key }) => resCounts[key]) - .map(({ key, label }) => ``) - .join(""); - const totalGroups = groups.length; - parts.push(`
- - - Format: - - ${fmtChips} - ${resChips.length ? `Resolution:${resChips}` : ""} - - Status: - - - - - ${vipCount ? `` : ""} - ${partsCount ? `` : ""} -
`); - } - - // Stats bar - parts.push(`
-
${escapeHtml(r.potential_reclaim_human || "0 B")}
Recoverable
-
${escapeHtml(String(r.group_count || 0))}
Duplicate Groups
-
${escapeHtml(String(totalCandidates))}
Delete Candidates
-
`); - if (riskCount) { - parts.push(`
${escapeHtml(String(riskCount))} risky group${riskCount !== 1 ? "s" : ""} are skipped by default. Review part-like filenames before adding them back to the delete queue.
`); - } - - // Roots hint - if (roots.length) { - parts.push(`
${escapeHtml(roots.join(" · "))}
`); - } - - // Group cards - if (!groups.length) { - parts.push(`
No cached duplicate groups found.
`); - } else { - const cards = []; - for (const [javId, group] of groups) { - const keep = group.keep || {}; - const deletions = group.delete_candidates || []; - const catalogs = group.catalog || []; - const reclaim = deletions.reduce((s, e) => s + (e.size || 0), 0); - const reclaimHuman = deletions.length && deletions[0].size_human - ? deletions.map((d) => d.size_human).join(" + ") - : ""; - const reclaimLabel = reclaimHuman - ? `−${escapeHtml(reclaimHuman)}` - : ""; - - const fmtKey = _groupFmtKey(keep, deletions); - const resKey = _groupResKey(keep, deletions); - const risks = group.risks || []; - const keepReason = group.keep_reason?.summary || ""; - - const rows = []; - if (risks.length) { - rows.push(`
Review before deleting: ${risks.map((risk) => escapeHtml(risk.summary || "multipart risk")).join("
")}
`); - } - rows.push(`
- KEEP - ${escapeHtml(dupePath(keep))} - ${keep.size_human ? `${escapeHtml(keep.size_human)}` : ""} -
`); - if (keepReason) { - rows.push(`
Suggested KEEP reason: ${escapeHtml(keepReason)}
`); - } - for (const d of deletions) { - rows.push(`
- DELETE? - ${escapeHtml(dupePath(d))} - ${d.size_human ? `${escapeHtml(d.size_human)}` : ""} -
`); - } - for (const c of catalogs) { - rows.push(`
- CATALOG - ${escapeHtml(dupePath(c))} - ${c.size_human ? `${escapeHtml(c.size_human)}` : ""} -
`); - } - - const hasClearJav = [keep, ...deletions].some((row) => /(?:^|[\\/])clearjav(?:[\\/]|$)/i.test(dupePath(row))); - cards.push(`
-
-
- ${escapeHtml(javId)} - ${_drBadges(keep, deletions, catalogs)} - ${reclaimLabel} -
-
${rows.join("")}
-
- -
`); - } - parts.push(`
${cards.join("")}
`); - } - - - // Variant alerts — bare ID + variant coexist (e.g. IBW-902 and IBW-902z both present) - const variantAlerts = r.variant_alerts || []; - if (variantAlerts.length) { - const alertCards = variantAlerts.map((alert) => { - const rows = (alert.files || []).map((f) => { - const detectedId = f.detected_id || f.jav_id || ""; - const isVariant = detectedId !== alert.bare_id; - const tag = isVariant - ? `${escapeHtml(detectedId)}` - : `BARE`; - return `
- ${tag} - ${escapeHtml(dupePath(f))} - ${f.size_human ? `${escapeHtml(f.size_human)}` : ""} -
`; - }).join(""); - return `
-
- ${escapeHtml(alert.bare_id)} - ⚠ variant — manual review -
-
${rows}
-
`; - }).join(""); - parts.push(`
-
⚠ ${variantAlerts.length} Variant Alert${variantAlerts.length !== 1 ? "s" : ""} — Same base ID, different product designator
-
${alertCards}
-
`); - } - - if ((r.skipped || []).length) { - const samples = (r.skipped || []).slice(0, 5) - .map((s) => `
${escapeHtml(s.name || s.path || "?")} · ${escapeHtml(s.reason || "unparsed ID")}
`) - .join(""); - parts.push(`
Skipped ${escapeHtml(String(r.skipped.length))} path(s) with no parseable ID${samples ? ":" : "."}
${samples}`); - } - - out.innerHTML = parts.join(""); - summary.textContent = `${r.group_count || 0} cached duplicate group(s) reviewed. Results are open in the review window.`; - openModal("dupe-review-modal"); -} - -function _drUpdateExecuteBtn() { - const confirmed = document.querySelectorAll("#dupe-review-modal-body .dr-card-wrap:not(.skipped):not(.dr-hidden) .dr-row.del.confirmed:not(.done)"); - const btn = document.getElementById("dupe-review-execute"); - const status = document.getElementById("dupe-review-confirm-status"); - const n = confirmed.length; - btn.textContent = `Execute Deletions (${n})`; - btn.disabled = n === 0; - status.textContent = n > 0 ? `${n} file${n !== 1 ? "s" : ""} queued for deletion — click to execute` : ""; -} - -// Search input -document.getElementById("dupe-review-modal-body").addEventListener("input", (e) => { - if (e.target.id !== "dr-search") return; - _drActiveSearch = e.target.value.trim().toLowerCase(); - _drApplyFilters(); - _drUpdateExecuteBtn(); -}); - -// Filter chips + toggle DELETE? rows on click -document.getElementById("dupe-review-modal-body").addEventListener("click", (e) => { - // Filter chip - const chip = e.target.closest(".dr-chip"); - if (chip) { - const ftype = chip.dataset.ftype; - const fval = chip.dataset.fval; - if (ftype === "fmt") { - _drActiveFmt = fval; - } else if (ftype === "res") { - _drActiveRes = fval; - } else if (ftype === "status") { - _drActiveStatus = fval; - } else if (ftype === "parts") { - _drActiveParts = fval; - } else if (ftype === "vip") { - _drActiveVip = fval; - } - document.querySelectorAll(`#dupe-review-modal-body .dr-chip[data-ftype='${ftype}']`).forEach(c => { - c.classList.toggle("active", c.dataset.fval === fval); - }); - _drApplyFilters(); - _drUpdateExecuteBtn(); - return; - } - - // Search input - if (e.target.id === "dr-search") return; // handled via input event below - - // Skip ear — toggle skipped on the wrap - const ear = e.target.closest(".dr-skip-ear"); - if (ear) { - ear.closest(".dr-card-wrap").classList.toggle("skipped"); - _drUpdateExecuteBtn(); - return; - } - - // Click KEEP row → full swap: this becomes DELETE?, pick a replacement KEEP - const keepRow = e.target.closest(".dr-row.keep"); - if (keepRow && !keepRow.classList.contains("done")) { - const card = keepRow.closest(".dr-card"); - // Prefer an unconfirmed del row as new KEEP (least disruptive), else first any del row - const newKeep = card.querySelector(".dr-row.del.unconfirmed:not(.done)") - || card.querySelector(".dr-row.del:not(.done)"); - if (!newKeep) return; - _drPromoteToKeep(newKeep); - _drDemoteToDelete(keepRow); - _drUpdateExecuteBtn(); - return; - } - - // DELETE? rows are not clickable — click the KEEP row to swap -}); - -document.getElementById("dupe-review-execute").addEventListener("click", async () => { - const rows = [...document.querySelectorAll("#dupe-review-modal-body .dr-card-wrap:not(.skipped):not(.dr-hidden) .dr-row.del.confirmed:not(.done)")]; - if (!rows.length) return; - const deleteRows = rows.filter((row) => row.dataset.fullPath); - if (!deleteRows.length) return; - const btn = document.getElementById("dupe-review-execute"); - const status = document.getElementById("dupe-review-confirm-status"); - const total = deleteRows.length; - btn.disabled = true; - let done = 0, failed = 0; - for (const [index, row] of deleteRows.entries()) { - const path = row.dataset.fullPath; - status.textContent = `Deleting ${index + 1}/${total}...`; - const res = await chrome.runtime.sendMessage({ type: "delete_batch", paths: [path] }); - if (!res?.ok && res?.error === "deletion is disabled in options") { - status.textContent = "Deletion is disabled - enable it in the Deletion tab first."; - btn.disabled = false; - return; - } - const r = (res?.results || [])[0] || { ok: false, error: res?.error || "delete failed" }; - const tag = row.querySelector(".dr-tag"); - if (r.ok) { - row.classList.remove("confirmed"); - row.classList.add("done"); - tag.textContent = "DELETED"; - done++; - } else { - row.classList.add("error"); - tag.textContent = "ERROR"; - row.title = r.error || "delete failed"; - failed++; - } - } - - const parts = []; - if (done) parts.push(`${done} deleted`); - if (failed) parts.push(`${failed} failed`); - status.textContent = parts.join(" · ") || "Nothing processed."; - _drUpdateExecuteBtn(); -}); - -document.getElementById("dupe-review-run").addEventListener("click", async () => { - const out = document.getElementById("dupe-review-modal-body"); - const executeBtn = document.getElementById("dupe-review-execute"); - out.textContent = "reviewing cached duplicate groups..."; - executeBtn.disabled = true; - executeBtn.textContent = "Execute Deletions (0)"; - document.getElementById("dupe-review-confirm-status").textContent = ""; - openModal("dupe-review-modal"); - renderDupeReview(await chrome.runtime.sendMessage({ type: "dupe-review" })); - _drUpdateExecuteBtn(); -}); - -document.getElementById("dupe-review-export").addEventListener("click", () => { - if (!lastDupeReview) return; - const blob = new Blob([JSON.stringify(lastDupeReview, null, 2)], { type: "application/json" }); - const url = URL.createObjectURL(blob); - const a = document.createElement("a"); - const stamp = new Date().toISOString().replace(/[:.]/g, "-"); - a.href = url; - a.download = `rclone-jav-dupe-review-${stamp}.json`; - a.click(); - setTimeout(() => URL.revokeObjectURL(url), 0); -}); -for (const id of ["dupe-review-modal-close", "dupe-review-modal-done"]) { - document.getElementById(id).addEventListener("click", () => closeModal("dupe-review-modal")); -} -document.getElementById("dupe-review-modal").addEventListener("click", (event) => { - if (event.target.id === "dupe-review-modal") closeModal("dupe-review-modal"); -}); - -// ---- Keep Ranking ---- - -const KR_DEFAULT_FMTS = ["mkv", "mp4", "wmv", "avi"]; -const KR_DEFAULT_VIP_FOLDERS = ["ClearJAV"]; - -function _krWireDraggableList(list) { - if (!list) return; - let dragSrc = null; - for (const item of list.querySelectorAll(".kr-fmt-item")) { - item.addEventListener("dragstart", (e) => { - dragSrc = item; - item.classList.add("dragging"); - e.dataTransfer.effectAllowed = "move"; - }); - item.addEventListener("dragend", () => { - item.classList.remove("dragging"); - list.querySelectorAll(".kr-fmt-item").forEach(i => i.classList.remove("drag-over")); - list.querySelectorAll(".kr-fmt-item").forEach((el, idx) => { - const pr = el.querySelector(".kr-fmt-priority"); - if (pr) pr.textContent = `#${idx + 1}`; - }); - }); - item.addEventListener("dragover", (e) => { - e.preventDefault(); - e.dataTransfer.dropEffect = "move"; - if (item !== dragSrc) item.classList.add("drag-over"); - }); - item.addEventListener("dragleave", () => item.classList.remove("drag-over")); - item.addEventListener("drop", (e) => { - e.preventDefault(); - item.classList.remove("drag-over"); - if (dragSrc && dragSrc !== item) { - const items = [...list.querySelectorAll(".kr-fmt-item")]; - const srcIdx = items.indexOf(dragSrc); - const dstIdx = items.indexOf(item); - if (srcIdx < dstIdx) list.insertBefore(dragSrc, item.nextSibling); - else list.insertBefore(dragSrc, item); - } - }); - } -} - -function _krRenderFmtList(fmts) { - const list = document.getElementById("kr-fmt-list"); - if (!list) return; - list.innerHTML = fmts.map((fmt, i) => - `
- - ${escapeHtml(fmt)} - #${i + 1} -
` - ).join(""); - _krWireDraggableList(list); -} - -function _krGetCurrentFmts() { - return [...document.querySelectorAll("#kr-fmt-list .kr-fmt-item")] - .map(el => el.dataset.fmt); -} - -function _krRenderVipList(folders) { - const list = document.getElementById("kr-vip-list"); - if (!list) return; - list.innerHTML = (folders || []).map((folder, i) => - `
- - ${escapeHtml(folder)} - - #${i + 1} -
` - ).join(""); - for (const btn of list.querySelectorAll(".kr-vip-remove")) { - btn.addEventListener("click", () => { - btn.closest(".kr-fmt-item")?.remove(); - list.querySelectorAll(".kr-fmt-item").forEach((el, idx) => { - const pr = el.querySelector(".kr-fmt-priority"); - if (pr) pr.textContent = `#${idx + 1}`; - }); - }); - } - _krWireDraggableList(list); -} - -function _krGetVipFolders() { - return [...document.querySelectorAll("#kr-vip-list .kr-fmt-item")] - .map((el) => el.dataset.folder) - .filter(Boolean); -} - -function _krAddVipFolder() { - const input = document.getElementById("kr-vip-add"); - const folder = input?.value.trim(); - if (!folder) return; - const current = _krGetVipFolders(); - if (!current.some((item) => item.toLowerCase() === folder.toLowerCase())) { - _krRenderVipList([...current, folder]); - } - input.value = ""; -} - -document.getElementById("kr-vip-add-btn")?.addEventListener("click", _krAddVipFolder); -document.getElementById("kr-vip-add")?.addEventListener("keydown", (event) => { - if (event.key === "Enter") { - event.preventDefault(); - _krAddVipFolder(); - } -}); - -async function loadKeepRanking() { - try { - const r = await chrome.runtime.sendMessage({ type: "get-keep-ranking" }); - if (!r || !r.ok) return; - const ranking = r.keep_ranking || {}; - const toleranceEl = document.getElementById("kr-tolerance"); - const resTagEl = document.getElementById("kr-res-tag"); - const longerNameEl = document.getElementById("kr-longer-name"); - if (toleranceEl) toleranceEl.value = ranking.size_tolerance_mib ?? 0; - if (resTagEl) resTagEl.checked = ranking.tiebreak_res_tag !== false; - if (longerNameEl) longerNameEl.checked = ranking.tiebreak_longer_name !== false; - _krRenderVipList(ranking.priority_folders || KR_DEFAULT_VIP_FOLDERS); - _krRenderFmtList(ranking.format_preference || KR_DEFAULT_FMTS); - } catch (e) { - // non-fatal — panel just shows defaults - _krRenderVipList(KR_DEFAULT_VIP_FOLDERS); - _krRenderFmtList(KR_DEFAULT_FMTS); - } -} - -document.getElementById("kr-save")?.addEventListener("click", async () => { - const status = document.getElementById("kr-save-status"); - const toleranceEl = document.getElementById("kr-tolerance"); - const resTagEl = document.getElementById("kr-res-tag"); - const longerNameEl = document.getElementById("kr-longer-name"); - const tolerance = parseFloat(toleranceEl?.value ?? "0"); - if (isNaN(tolerance) || tolerance < 0) { - status.textContent = "Size tolerance must be 0 or a positive number."; - status.className = "kr-save-status err"; - return; - } - const ranking = { - priority_folders: _krGetVipFolders(), - size_tolerance_mib: tolerance, - format_preference: _krGetCurrentFmts(), - tiebreak_res_tag: resTagEl?.checked !== false, - tiebreak_longer_name: longerNameEl?.checked !== false, - }; - status.textContent = "Saving…"; - status.className = "kr-save-status"; - try { - const r = await chrome.runtime.sendMessage({ type: "save-keep-ranking", keep_ranking: ranking }); - if (r?.ok) { - status.textContent = "Saved — next dupe review will use the updated ranking."; - status.className = "kr-save-status ok"; - } else { - status.textContent = "Error: " + (r?.error || "unknown"); - status.className = "kr-save-status err"; - } - } catch (e) { - status.textContent = "Error: " + e.message; - status.className = "kr-save-status err"; - } -}); - -// Load on page open -loadKeepRanking(); // ---- Library Issues ----