const $status = document.getElementById("status"); const $output = document.getElementById("output"); const $deleteBtn = document.getElementById("delete-btn"); const $undoBtn = document.getElementById("undo-btn"); const $cacheBanner = document.getElementById("cache-banner"); const $filterBar = document.getElementById("filter-bar"); const $modeLive = document.getElementById("mode-live"); const $modeCache = document.getElementById("mode-cache"); const $pauseScan = document.getElementById("pause-scan"); let lastResult = null; let settings = null; let activeFilter = "all"; // "all" | "Source" | "Target" | "Catalog" function syncModeToggle() { const live = settings && settings.quickMode !== false; $modeLive.classList.toggle("active", live); $modeCache.classList.toggle("active", !live); $modeLive.setAttribute("aria-pressed", live ? "true" : "false"); $modeCache.setAttribute("aria-pressed", !live ? "true" : "false"); } function syncPauseButton() { const paused = !!(settings && settings.scanPaused); $pauseScan.classList.toggle("paused", paused); $pauseScan.textContent = paused ? "▶" : "⏸"; $pauseScan.title = paused ? "Resume scanning" : "Pause scanning"; $pauseScan.setAttribute("aria-pressed", paused ? "true" : "false"); } function renderPausedState() { setStatus("Scanning paused", "err"); $output.innerHTML = ""; const div = document.createElement("div"); div.className = "empty"; div.textContent = "Press ▶ to resume scans."; $output.appendChild(div); $deleteBtn.style.display = "none"; $undoBtn.style.display = "none"; } async function setScanPaused(paused) { const s = await chrome.runtime.sendMessage({ type: "get-settings" }) || {}; settings = Object.assign({}, s, { scanPaused: paused }); await chrome.storage.sync.set({ settings }); chrome.runtime.sendMessage({ type: "settings-changed" }); syncPauseButton(); if (paused) renderPausedState(); else if (manualMode && $searchInput.value.trim()) runManualSearch(); else runCheck(true); } async function setSearchMode(mode) { const quickMode = mode === "live"; if (settings && settings.quickMode === quickMode) return; const s = await chrome.runtime.sendMessage({ type: "get-settings" }) || {}; settings = Object.assign({}, s, { quickMode }); await chrome.storage.sync.set({ settings }); chrome.runtime.sendMessage({ type: "settings-changed" }); syncModeToggle(); if (manualMode && $searchInput.value.trim()) runManualSearch(); else runCheck(true); } function setStatus(text, cls = "") { $status.className = cls; if (cls === "loading") { $status.innerHTML = `${text}`; } else { $status.textContent = text; } } function showSkeleton(rows = 2) { $output.innerHTML = ""; for (let i = 0; i < rows; i++) { const div = document.createElement("div"); div.className = "skeleton"; div.innerHTML = `
`; $output.appendChild(div); } } function fmtAge(ts) { if (!ts) return ""; const s = Math.round((Date.now() - ts) / 1000); if (s < 60) return `${s}s ago`; if (s < 3600) return `${Math.round(s / 60)}m ago`; return `${Math.round(s / 3600)}h ago`; } function escapeHtml(s) { return String(s).replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[c])); } function fmtMs(n) { return Number.isFinite(n) ? `${n}ms` : "?"; } function renderTimings(r) { const t = r && r.timings; if (!t) return; const div = document.createElement("div"); div.className = "timing-strip"; const engineLabel = Number.isFinite(t.host_cached_ms) ? "HOST" : "RC-JAV"; const engineMs = Number.isFinite(t.host_cached_ms) ? t.host_cached_ms : t.host_rcjav_ms; const matchMs = t.cache_match_ms ?? t.match_ms; const metrics = [ ["TOTAL", fmtMs(t.total_ms)], [engineLabel, fmtMs(engineMs)], ["EXTRACT", fmtMs(t.extract_ms)], ["MATCH", fmtMs(matchMs)], ]; div.innerHTML = metrics.map(([label, value]) => `
${escapeHtml(label)}
${escapeHtml(value)}
`).join(""); $output.appendChild(div); } function renderSearchMode(_r) { // Mode chip removed — the LIVE/CACHE toggle in the header already shows the mode. } function buildHitCard(h) { const filename = h.path.split("/").pop(); const idx = h.full_path.lastIndexOf("/"); const dir = idx >= 0 ? h.full_path.slice(0, idx) : h.full_path; const srcCls = h.source === "Source" ? "src source" : h.source === "Catalog" ? "src catalog" : "src"; const confidence = h.match_confidence ? ` · ${h.match_confidence}` : ""; const reason = h.match_reason ? `${escapeHtml(h.match_reason)}` : ""; const div = document.createElement("div"); div.className = "hit"; div.innerHTML = `
${escapeHtml(filename)}
Path: ${escapeHtml(dir)}
${escapeHtml(h.source.toUpperCase())}${escapeHtml(h.size_human)}${reason}
`; return div; } function renderFilterBar(structured) { $filterBar.innerHTML = ""; if (!structured || structured.length < 2) { $filterBar.style.display = "none"; return; } const sources = [...new Set(structured.map((h) => h.source))]; if (sources.length < 2) { $filterBar.style.display = "none"; return; } $filterBar.style.display = ""; const filters = ["all", ...sources]; for (const f of filters) { const chip = document.createElement("span"); chip.className = "filter-chip" + (activeFilter === f ? " active" : ""); chip.textContent = f === "all" ? "All" : f; chip.addEventListener("click", () => { activeFilter = f; renderHits(lastResult && lastResult.structured); for (const c of $filterBar.children) c.classList.toggle("active", c.textContent === (f === "all" ? "All" : f)); }); $filterBar.appendChild(chip); } } function renderHits(structured) { // Clear existing hit cards but keep mode/timing lines for (const el of [...$output.children]) { if (el.classList.contains("hit") || el.classList.contains("empty")) el.remove(); } const hits = structured || []; const filtered = activeFilter === "all" ? hits : hits.filter((h) => h.source === activeFilter); if (filtered.length) { const anchor = $output.querySelector(".timing-strip") || $output.querySelector(".timings"); for (const h of filtered) { const card = buildHitCard(h); if (anchor) $output.insertBefore(card, anchor); else $output.appendChild(card); } } else { const div = document.createElement("div"); div.className = "empty"; div.textContent = filtered.length === 0 && activeFilter !== "all" ? `No ${activeFilter} results` : "no matches"; const anchor = $output.querySelector(".timing-strip") || $output.querySelector(".timings"); if (anchor) $output.insertBefore(div, anchor); else $output.appendChild(div); } } function isNativeRegistrationIssue(result) { return result && (result.error_kind === "forbidden" || result.error_kind === "not_found"); } function renderNativeSetupGuide(result) { const div = document.createElement("div"); div.className = "setup-guide"; const forbidden = result.error_kind === "forbidden"; div.innerHTML = ` ${forbidden ? "Register this extension ID on this PC" : "Native host registration is missing on this PC"}
${forbidden ? "Brave found the host, but this copy of the extension is not allowed to launch it yet." : "Brave cannot find the rclone-jav native host yet."}
${result.extension_id ? `` : ""}
`; div.querySelector("[data-open-native-setup]").addEventListener("click", async () => { await chrome.storage.local.set({ optionsActivePane: "diagnostics", pendingNativeSetupIssue: { error: result.error || result.reason || "native host registration failed", error_kind: result.error_kind, extension_id: result.extension_id || "", }, }); chrome.runtime.openOptionsPage(); }); const copy = div.querySelector("[data-copy-extension-id]"); if (copy) { copy.addEventListener("click", async () => { await navigator.clipboard.writeText(copy.dataset.copyExtensionId || ""); copy.textContent = "Copied"; setTimeout(() => { copy.textContent = "Copy Extension ID"; }, 1200); }); } $output.appendChild(div); } function renderResult(r) { lastResult = r; activeFilter = "all"; $output.innerHTML = ""; $filterBar.style.display = "none"; if (!r) { setStatus("no response", "err"); return; } const tag = r.cached ? ` [session ${fmtAge(r.ts)}]` : ""; if (!r.ok) { setStatus("✗ " + (r.reason || "error") + tag, "err"); $output.innerHTML = ""; if (r.no_match_title || r.no_match_detail) { const div = document.createElement("div"); div.className = "empty no-match-detail"; div.innerHTML = `${escapeHtml(r.no_match_title || "No result")}${r.no_match_detail ? `
${escapeHtml(r.no_match_detail)}
` : ""}`; $output.appendChild(div); } if (r.stderr || r.error) { const div = document.createElement("div"); div.className = "err"; div.textContent = r.stderr || r.error; $output.appendChild(div); } if (isNativeRegistrationIssue(r)) renderNativeSetupGuide(r); $deleteBtn.style.display = "none"; $undoBtn.style.display = "none"; return; } const idShown = r.id || (r.structured && r.structured[0] && r.structured[0].jav_id) || "?"; if (r.hits > 0) setStatus(`✓ ${idShown} — ${r.hits} hit(s)${tag}`, "hit"); else setStatus(`✗ ${idShown} — NOT FOUND${tag}`, "miss"); // Render mode + timings first (renderHits inserts before these) renderSearchMode(r); renderTimings(r); // Render filter bar then hit cards renderFilterBar(r.structured); if (r.structured && r.structured.length) { renderHits(r.structured); } else { const div = document.createElement("div"); div.className = "empty no-match-detail"; div.innerHTML = `${escapeHtml(r.no_match_title || "No matches")}${r.no_match_detail ? `
${escapeHtml(r.no_match_detail)}
` : ""}`; $output.insertBefore(div, $output.firstChild); } const canDelete = settings && settings.enableDelete && r.structured && r.structured.length > 1; $deleteBtn.style.display = canDelete ? "" : "none"; $deleteBtn.textContent = "DELETE"; $undoBtn.style.display = (settings && settings.enableDelete && settings.deleteMode === "trash") ? "" : "none"; } function runCheck(force = false) { if (settings && settings.scanPaused) { renderPausedState(); return; } setStatus("Scanning…", "loading"); showSkeleton(2); $deleteBtn.style.display = "none"; $undoBtn.style.display = "none"; chrome.runtime.sendMessage({ type: "check-tab", force }, (r) => { if (chrome.runtime.lastError) { setStatus("error: " + chrome.runtime.lastError.message, "err"); $output.innerHTML = ""; return; } renderResult(r); }); } // ----- delete modal ----- const $overlay = document.getElementById("modal-overlay"); const $list = document.getElementById("modal-list"); const $confirmWrap = document.getElementById("modal-confirm"); const $target = document.getElementById("modal-target"); const $confirmId = document.getElementById("confirm-id"); const $confirmInput = document.getElementById("modal-confirm-input"); const $mode = document.getElementById("modal-mode"); const $modalDelete = document.getElementById("modal-delete"); const $modalStatus = document.getElementById("modal-status"); let chosenHit = null; let expectedId = ""; function openDeleteModal() { if (!lastResult || !lastResult.structured) return; $list.innerHTML = ""; chosenHit = null; $confirmWrap.style.display = "none"; $modalDelete.disabled = true; $modalStatus.textContent = ""; $confirmInput.value = ""; for (const h of lastResult.structured) { const filename = h.path.split("/").pop(); const idx = h.full_path.lastIndexOf("/"); const dir = idx >= 0 ? h.full_path.slice(0, idx) : h.full_path; const srcCls = h.source === "Source" ? "src source" : h.source === "Catalog" ? "src catalog" : "src"; const row = document.createElement("div"); row.className = "hit"; row.innerHTML = `
${escapeHtml(filename)}
Path: ${escapeHtml(dir)}
${escapeHtml(h.source.toUpperCase())}${escapeHtml(h.size_human)}
`; row.addEventListener("click", () => selectHit(h, row)); $list.appendChild(row); } $overlay.style.display = "block"; } function selectHit(h, row) { chosenHit = h; for (const el of $list.children) el.classList.remove("selected"); row.classList.add("selected"); $confirmWrap.style.display = ""; $target.textContent = h.full_path; // Require typing the full filename (basename incl. extension) — strongest unique signal. expectedId = h.path.split("/").pop(); $confirmId.textContent = expectedId; const modeText = settings.deleteMode === "permanent" ? "PERMANENTLY DELETE" : `move to trash: ${settings.trashDir}`; $mode.innerHTML = `
Mode: ${escapeHtml(modeText)}
Source: ${escapeHtml(h.source || "?")}
Remote/prefix: ${escapeHtml(h.remote || "?")}
Host safety: path must be inside configured rc-jav source/target or trash prefixes.
`; $confirmInput.value = ""; $confirmInput.focus(); $modalDelete.disabled = true; } $confirmInput.addEventListener("input", () => { // Case-insensitive, exact filename match. $modalDelete.disabled = $confirmInput.value.trim().toLowerCase() !== expectedId.toLowerCase(); }); document.getElementById("modal-cancel").addEventListener("click", () => { $overlay.style.display = "none"; }); $modalDelete.addEventListener("click", () => { if (!chosenHit) return; $modalDelete.disabled = true; $modalStatus.textContent = "deleting…"; chrome.runtime.sendMessage({ type: "delete-file", path: chosenHit.full_path }, (r) => { if (!r) { $modalStatus.textContent = "no response"; return; } if (!r.ok) { $modalStatus.textContent = "error: " + (r.error || r.stderr || "unknown"); return; } $modalStatus.textContent = (settings.deleteMode === "permanent" ? "deleted" : "moved to: " + r.dst); // Refresh underlying tab result setTimeout(() => { $overlay.style.display = "none"; runCheck(true); }, 800); }); }); // ----- search history ----- const HISTORY_KEY = "searchHistory"; const HISTORY_MAX = 20; const $historyBar = document.getElementById("history-bar"); const $historyChips = document.getElementById("history-chips"); async function loadHistory() { const got = await chrome.storage.local.get(HISTORY_KEY); return Array.isArray(got[HISTORY_KEY]) ? got[HISTORY_KEY] : []; } async function pushHistory(id) { const list = await loadHistory(); const filtered = list.filter((x) => x.toLowerCase() !== id.toLowerCase()); const updated = [id, ...filtered].slice(0, HISTORY_MAX); await chrome.storage.local.set({ [HISTORY_KEY]: updated }); renderHistory(updated); } function renderHistory(list) { $historyChips.innerHTML = ""; if (!list || !list.length) { $historyBar.style.display = "none"; return; } $historyBar.style.display = ""; for (const id of list) { const chip = document.createElement("span"); chip.className = "history-chip"; chip.textContent = id; chip.title = `Search ${id}`; chip.addEventListener("click", () => { $searchInput.value = id; runManualSearch(); }); $historyChips.appendChild(chip); } } document.getElementById("history-clear").addEventListener("click", async () => { await chrome.storage.local.set({ [HISTORY_KEY]: [] }); renderHistory([]); }); // ----- manual search ----- const $searchInput = document.getElementById("search-input"); const $searchGo = document.getElementById("search-go"); const $searchClear = document.getElementById("search-clear"); let manualMode = false; // true while popup is showing manual-search results function runManualSearch() { const raw = $searchInput.value.trim(); if (!raw) return; if (settings && settings.scanPaused) { renderPausedState(); return; } manualMode = true; setStatus(`Searching ${raw}…`, "loading"); showSkeleton(2); $deleteBtn.style.display = "none"; $undoBtn.style.display = "none"; const t0 = performance.now(); chrome.runtime.sendMessage({ type: "manual-query", action: "search", id: raw, quick: !!(settings && settings.quickMode), }, (r) => { if (chrome.runtime.lastError) { setStatus("error: " + chrome.runtime.lastError.message, "err"); $output.innerHTML = ""; return; } // The host search response uses the same shape as check-tab, so reuse the // renderer. Synthesize a `timings.total_ms` so the timing chip still works. const result = Object.assign({}, r, { id: raw, timings: Object.assign({ total_ms: Math.round(performance.now() - t0) }, r && r.timings ? r.timings : {}), }); renderResult(result); if (result && result.ok) pushHistory(raw); }); } $searchGo.addEventListener("click", runManualSearch); $searchInput.addEventListener("keydown", (e) => { if (e.key === "Enter") { e.preventDefault(); runManualSearch(); } }); $searchClear.addEventListener("click", () => { $searchInput.value = ""; manualMode = false; runCheck(false); $searchInput.focus(); }); // ----- undo modal ----- const $undoOverlay = document.getElementById("undo-overlay"); const $undoList = document.getElementById("undo-list"); const $undoStatus = document.getElementById("undo-status"); function openUndoModal() { $undoList.innerHTML = `
Loading…
`; $undoStatus.textContent = ""; $undoOverlay.style.display = "block"; chrome.runtime.sendMessage({ type: "recent-deletes", limit: 20 }, (r) => { if (chrome.runtime.lastError) { $undoList.innerHTML = `
${escapeHtml(chrome.runtime.lastError.message)}
`; return; } if (!r || !r.ok) { $undoList.innerHTML = `
${escapeHtml((r && r.error) || "unknown error")}
`; return; } const entries = (r.entries || []); if (!entries.length) { $undoList.innerHTML = `
No recent trash deletes found.
`; return; } $undoList.innerHTML = ""; for (const e of entries) { const origName = (e.path || "").split("/").pop(); const age = fmtAge(e.ts ? new Date(e.ts).getTime() : null); const row = document.createElement("div"); row.className = "undo-entry"; row.innerHTML = `
${escapeHtml(origName)}
Orig: ${escapeHtml(e.path || "?")}
Trash: ${escapeHtml(e.dst || "?")}
${escapeHtml(age)}
`; row.querySelector(".undo-row-btn").addEventListener("click", () => doUndo(e, row)); $undoList.appendChild(row); } }); } function doUndo(e, row) { const btn = row.querySelector(".undo-row-btn"); btn.disabled = true; btn.textContent = "…"; $undoStatus.textContent = ""; chrome.runtime.sendMessage({ type: "undo-delete", dst: e.dst, path: e.path }, (r) => { if (!r) { btn.disabled = false; btn.textContent = "↶ Restore"; $undoStatus.textContent = "no response"; return; } if (!r.ok) { btn.disabled = false; btn.textContent = "↶ Restore"; $undoStatus.textContent = "error: " + (r.error || r.stderr || "unknown"); return; } btn.textContent = "✓ restored"; row.style.opacity = "0.5"; $undoStatus.textContent = "Restored to: " + (e.path || "?"); // Refresh the main result after a short pause setTimeout(() => { $undoOverlay.style.display = "none"; runCheck(true); }, 1200); }); } document.getElementById("undo-cancel").addEventListener("click", () => { $undoOverlay.style.display = "none"; }); $undoBtn.addEventListener("click", openUndoModal); // ----- wiring ----- document.getElementById("recheck").addEventListener("click", async () => { if (settings && settings.scanPaused) { await setScanPaused(false); return; } if (manualMode && $searchInput.value.trim()) runManualSearch(); else runCheck(true); }); document.getElementById("open-options").addEventListener("click", () => chrome.runtime.openOptionsPage()); $modeLive.addEventListener("click", () => setSearchMode("live")); $modeCache.addEventListener("click", () => setSearchMode("cache")); $pauseScan.addEventListener("click", () => setScanPaused(!(settings && settings.scanPaused))); $deleteBtn.addEventListener("click", openDeleteModal); document.getElementById("ping").addEventListener("click", () => { setStatus("pinging host…"); $output.textContent = ""; chrome.runtime.sendMessage({ type: "ping-host" }, (r) => { if (!r || !r.ok) { setStatus("host unreachable", "err"); $output.textContent = r?.error || ""; return; } setStatus("host ok: " + (r.version || "unknown"), "hit"); const rows = [ ["Host version", r.version || "?"], ["rc-jav path", r.rc_jav || "?"], ["Script exists", r.rc_jav_exists ? "✓ yes" : "✗ not found"], ["Path source", r.rc_jav_overridden ? "options override" : "built-in default"], ["Python", r.python || "?"], ]; const div = document.createElement("div"); div.style.cssText = "font-size:12px;line-height:1.7;"; div.innerHTML = rows.map(([k, v]) => `
${k} ${v}
` ).join(""); $output.appendChild(div); }); }); function loadCacheBanner() { chrome.runtime.sendMessage({ type: "cache-status" }, (r) => { if (chrome.runtime.lastError || !r || !r.ok) return; // silent fail — banner optional if (!r.cache_exists) { $cacheBanner.className = "no-cache"; $cacheBanner.textContent = "⚠ No cache — searches use quick (live) mode only. Run --scan to build."; $cacheBanner.style.display = ""; return; } const staleRemotes = (r.remotes || []).filter((x) => x.stale); if (!staleRemotes.length) { $cacheBanner.style.display = "none"; return; } const oldest = staleRemotes.reduce((a, b) => (b.age_hours || 0) > (a.age_hours || 0) ? b : a, staleRemotes[0]); const ageH = oldest.age_hours != null ? Math.round(oldest.age_hours) : "?"; $cacheBanner.className = ""; $cacheBanner.textContent = `⚠ Cache is ${ageH}h old (${staleRemotes.length} remote${staleRemotes.length > 1 ? "s" : ""} stale). Consider running --scan.`; $cacheBanner.style.display = ""; }); } const $profileSelect = document.getElementById("profile-select"); $profileSelect.addEventListener("change", async () => { const newProfile = $profileSelect.value; // Save activeProfile to storage so background.js picks it up on next search. const s = await chrome.runtime.sendMessage({ type: "get-settings" }); await chrome.storage.sync.set({ settings: Object.assign({}, s, { activeProfile: newProfile }) }); settings = Object.assign({}, s, { activeProfile: newProfile }); // Re-run current search with new profile if (manualMode && $searchInput.value.trim()) runManualSearch(); else runCheck(true); }); function renderProfileSelector(s) { const profiles = s.profiles || []; if (!profiles.length) { $profileSelect.style.display = "none"; return; } $profileSelect.innerHTML = ""; const def = document.createElement("option"); def.value = ""; def.textContent = "Default"; $profileSelect.appendChild(def); for (const p of profiles) { const opt = document.createElement("option"); opt.value = p.name; opt.textContent = p.name; $profileSelect.appendChild(opt); } $profileSelect.value = s.activeProfile || ""; $profileSelect.style.display = ""; } (async () => { settings = await chrome.runtime.sendMessage({ type: "get-settings" }) || {}; syncModeToggle(); syncPauseButton(); renderProfileSelector(settings); loadCacheBanner(); renderHistory(await loadHistory()); if (settings.scanPaused) { renderPausedState(); return; } if (settings.triggers?.toolbarClick !== false) { runCheck(false); } else { setStatus("toolbar auto-check disabled"); $output.innerHTML = ""; const div = document.createElement("div"); div.className = "empty"; div.textContent = "Use Re-Scan to check this page."; $output.appendChild(div); } })();