const KEYS = [
"autoEveryPage",
"autoKnownSites",
"autoPageLoad",
"autoSpaNavigation",
"toolbarClick",
"contextMenu",
"keyboardShortcut",
];
// MUST match DEFAULT_SETTINGS in background.js.
const DEFAULT_TRIGGERS = {
autoEveryPage: false,
autoKnownSites: false,
autoPageLoad: true,
autoSpaNavigation: true,
toolbarClick: true,
contextMenu: true,
keyboardShortcut: true,
};
// ---------- sidebar nav ----------
function activatePane(pane) {
if (pane === "backup") pane = "paths";
if (pane === "review") pane = "maintenance";
const item = document.querySelector(`.side .item[data-pane="${pane}"]`) || document.querySelector('.side .item[data-pane="triggers"]');
if (!item) return;
document.querySelectorAll(".side .item").forEach((i) => i.classList.remove("active"));
item.classList.add("active");
document.querySelectorAll(".pane").forEach((p) => p.classList.remove("active"));
const paneEl = document.getElementById("pane-" + item.dataset.pane);
if (paneEl) paneEl.classList.add("active");
}
for (const item of document.querySelectorAll(".side .item")) {
item.addEventListener("click", async () => {
activatePane(item.dataset.pane);
await chrome.storage.local.set({ optionsActivePane: item.dataset.pane });
});
}
function getActionScrollContainer(element) {
for (let node = element?.parentElement; node; node = node.parentElement) {
const overflowY = getComputedStyle(node).overflowY;
if ((overflowY === "auto" || overflowY === "scroll") && node.scrollHeight > node.clientHeight) {
return node;
}
}
return document.scrollingElement || document.documentElement;
}
function nextAnimationFrame() {
return new Promise((resolve) => requestAnimationFrame(resolve));
}
function openModal(id) {
const modal = document.getElementById(id);
if (!modal) return;
modal.classList.add("open");
modal.setAttribute("aria-hidden", "false");
}
function closeModal(id) {
const modal = document.getElementById(id);
if (!modal) return;
modal.classList.remove("open");
modal.setAttribute("aria-hidden", "true");
}
async function keepActionViewport(action, run) {
const beforeTop = action?.getBoundingClientRect().top;
const scroller = getActionScrollContainer(action);
try {
return await run();
} finally {
await nextAnimationFrame();
await nextAnimationFrame();
if (!action?.isConnected || !Number.isFinite(beforeTop)) return;
const delta = action.getBoundingClientRect().top - beforeTop;
if (Math.abs(delta) < 1) return;
if (scroller === document.scrollingElement || scroller === document.documentElement) {
window.scrollBy(0, delta);
} else {
scroller.scrollTop += delta;
}
}
}
// ---------- overlay tabs ----------
for (const tab of document.querySelectorAll(".otab")) {
tab.addEventListener("click", () => {
document.querySelectorAll(".otab").forEach((t) => t.classList.remove("active"));
document.querySelectorAll(".otab-panel").forEach((p) => p.classList.remove("active"));
tab.classList.add("active");
document.getElementById("otab-" + tab.dataset.otab).classList.add("active");
});
}
// ---------- settings load / save ----------
async function load() {
const { settings = {} } = await chrome.storage.sync.get("settings");
const t = Object.assign({}, DEFAULT_TRIGGERS, settings.triggers || {});
for (const k of KEYS) document.getElementById(k).checked = !!t[k];
document.getElementById("quickMode").checked = settings.quickMode !== false;
document.getElementById("cacheStaleHours").value = Math.max(1, Number(settings.cacheStaleHours) || 24);
document.getElementById("showOverlay").checked = settings.showOverlay !== false;
// Overlay options
const pos = settings.overlayPosition || "top-right";
for (const r of document.querySelectorAll('input[name="overlayPosition"]')) {
r.checked = r.value === pos;
}
syncOverlayPosChips();
document.getElementById("overlayDuration").value = settings.overlayDuration || 5;
document.getElementById("overlayGlow").checked = !!settings.overlayGlow;
document.getElementById("overlayGlowColor").value = settings.overlayGlowColor || "#6ec1ff";
document.getElementById("overlayGlowBlur").value = settings.overlayGlowBlur ?? 10;
document.getElementById("overlayGlowSpread").value = settings.overlayGlowSpread ?? 0;
document.getElementById("overlayGlowOpacity").value = settings.overlayGlowOpacity ?? 0.35;
// no-match overlay
document.getElementById("noMatchOverlay").checked = !!settings.noMatchOverlay;
const nmPos = settings.noMatchPosition || "top-right";
for (const r of document.querySelectorAll('input[name="noMatchPosition"]')) {
r.checked = r.value === nmPos;
}
syncNoMatchPosChips();
document.getElementById("noMatchDuration").value = settings.noMatchDuration || 5;
document.getElementById("noMatchGlow").checked = !!settings.noMatchGlow;
document.getElementById("noMatchGlowColor").value = settings.noMatchGlowColor || "#ff6666";
document.getElementById("noMatchGlowBlur").value = settings.noMatchGlowBlur ?? 10;
document.getElementById("noMatchGlowSpread").value = settings.noMatchGlowSpread ?? 0;
document.getElementById("noMatchGlowOpacity").value = settings.noMatchGlowOpacity ?? 0.35;
if (typeof updateOverlayPreview === "function") updateOverlayPreview();
if (typeof updateNoMatchPreview === "function") updateNoMatchPreview();
if (typeof updateMutualExclusion === "function") updateMutualExclusion();
document.getElementById("knownSitePatterns").value = (settings.knownSitePatterns || []).join("\n");
renderAdapters(settings.siteAdapters || []);
renderNormalizers(settings.idNormalizers || []);
renderPartDetectors(settings.partPatterns || []);
document.getElementById("enableDelete").checked = !!settings.enableDelete;
const mode = settings.deleteMode === "permanent" ? "permanent" : "trash";
document.getElementById("deleteModeTrash").checked = mode === "trash";
document.getElementById("deleteModePerm").checked = mode === "permanent";
syncRadioChips();
document.getElementById("trashDir").value = settings.trashDir || "cq:personal-files/.rclone-jav-trash";
document.getElementById("rcjavPath").value = settings.rcjavPath || "";
renderProfiles(settings.profiles || []);
updateSectionSummaries();
syncDeletionControls();
}
async function save(e) {
const { settings: existingSettings = {} } = await chrome.storage.sync.get("settings");
const triggers = {};
for (const k of KEYS) triggers[k] = document.getElementById(k).checked;
const knownSitePatterns = document.getElementById("knownSitePatterns").value
.split(/[\n,]/).map((s) => s.trim()).filter(Boolean);
const quickMode = document.getElementById("quickMode").checked;
const cacheStaleHours = Math.max(1, Math.min(8760, parseInt(document.getElementById("cacheStaleHours").value, 10) || 24));
const showOverlay = document.getElementById("showOverlay").checked;
const overlayPosition = (document.querySelector('input[name="overlayPosition"]:checked') || {}).value || "top-right";
const overlayDuration = Math.max(1, parseInt(document.getElementById("overlayDuration").value, 10) || 5);
const overlayGlow = document.getElementById("overlayGlow").checked;
const overlayGlowColor = document.getElementById("overlayGlowColor").value || "#6ec1ff";
const overlayGlowBlur = parseInt(document.getElementById("overlayGlowBlur").value, 10);
const overlayGlowSpread = parseInt(document.getElementById("overlayGlowSpread").value, 10);
const overlayGlowOpacity = parseFloat(document.getElementById("overlayGlowOpacity").value);
const noMatchOverlay = document.getElementById("noMatchOverlay").checked;
const noMatchPosition = (document.querySelector('input[name="noMatchPosition"]:checked') || {}).value || "top-right";
const noMatchDuration = Math.max(1, parseInt(document.getElementById("noMatchDuration").value, 10) || 5);
const noMatchGlow = document.getElementById("noMatchGlow").checked;
const noMatchGlowColor = document.getElementById("noMatchGlowColor").value || "#ff6666";
const noMatchGlowBlur = parseInt(document.getElementById("noMatchGlowBlur").value, 10);
const noMatchGlowSpread = parseInt(document.getElementById("noMatchGlowSpread").value, 10);
const noMatchGlowOpacity = parseFloat(document.getElementById("noMatchGlowOpacity").value);
const siteAdapters = readAdapters();
const idNormalizers = readNormalizers();
const partPatterns = readPartDetectors();
const enableDelete = document.getElementById("enableDelete").checked;
const deleteMode = document.getElementById("deleteModePerm").checked ? "permanent" : "trash";
const trashDir = document.getElementById("trashDir").value.trim() || "cq:personal-files/.rclone-jav-trash";
const rcjavPath = document.getElementById("rcjavPath").value.trim();
const payload = {
triggers, knownSitePatterns, quickMode, cacheStaleHours,
showOverlay, overlayPosition, overlayDuration, overlayGlow, overlayGlowColor, overlayGlowBlur, overlayGlowSpread, overlayGlowOpacity,
noMatchOverlay, noMatchPosition, noMatchDuration, noMatchGlow, noMatchGlowColor, noMatchGlowBlur, noMatchGlowSpread, noMatchGlowOpacity,
siteAdapters, idNormalizers, partPatterns, enableDelete, deleteMode, trashDir, rcjavPath,
profiles: readProfiles(),
activeProfile: existingSettings.activeProfile || "",
scanPaused: !!existingSettings.scanPaused,
};
const btn = e && e.target ? e.target : null;
const saved = btn && btn.nextElementSibling;
try {
await chrome.storage.sync.set({ settings: payload });
} catch (err) {
// chrome.storage.sync has 8 KB/item + 100 KB total quota. Long adapter or
// normalizer lists can blow it; without this try/catch the rejection is
// silently swallowed and the "Saved." chip never appears.
if (saved && saved.classList.contains("saved")) {
saved.textContent = "Save failed: " + (err.message || String(err));
saved.classList.add("show", "error");
setTimeout(() => { saved.classList.remove("show", "error"); saved.textContent = "Saved."; }, 4000);
} else {
alert("Failed to save settings: " + (err.message || String(err)));
}
return;
}
chrome.runtime.sendMessage({ type: "settings-changed" });
updateSectionSummaries();
// Show "Saved." chip on the button that triggered this
if (saved && saved.classList.contains("saved")) {
saved.classList.add("show");
setTimeout(() => saved.classList.remove("show"), 1500);
}
}
for (const btn of document.querySelectorAll(".save-btn")) {
btn.addEventListener("click", save);
}
function plural(n, word) {
return `${n} ${word}${n === 1 ? "" : "s"}`;
}
function setNote(id, html, kind = "") {
const el = document.getElementById(id);
if (!el) return;
el.className = "section-note" + (kind ? ` ${kind}` : "");
el.innerHTML = html;
}
function updateSectionSummaries() {
const knownSites = document.getElementById("knownSitePatterns").value
.split(/[\n,]/).map((s) => s.trim()).filter(Boolean);
const triggersOn = KEYS.filter((k) => document.getElementById(k)?.checked);
const autoOn = document.getElementById("autoEveryPage").checked || document.getElementById("autoKnownSites").checked;
setNote("trigger-summary",
`${autoOn ? "Auto-check is enabled" : "Auto-check is off"} · ${plural(knownSites.length, "known site")} · ${plural(triggersOn.length, "manual/navigation control")} enabled`,
autoOn ? "" : "warn");
const match = document.getElementById("showOverlay").checked;
const noMatch = document.getElementById("noMatchOverlay").checked;
setNote("overlay-summary", `Match overlay ${match ? "on" : "off"} · no-match overlay ${noMatch ? "on" : "off"} · positions are kept separate to avoid overlap`, match || noMatch ? "" : "warn");
const adapters = readAdapters();
setNote("adapter-summary", `${plural(adapters.length, "custom adapter")} configured · built-in ClearJAV preset always runs`, adapters.length ? "" : "info");
const normalizers = readNormalizers();
const partPatterns = readPartDetectors();
setNote("normalizer-summary", `${plural(normalizers.length, "custom normalizer")} · ${plural(partPatterns.length, "custom part detector")} · built-in FC2 and multipart rules active`, normalizers.length || partPatterns.length ? "" : "info");
const quick = document.getElementById("quickMode").checked;
setNote("search-summary", quick ? "LIVE mode: single-ID lookups query rclone directly and bypass cache." : "CACHE mode: lookups use cache.json; rebuild cache after library or ID-normalization changes.");
const path = document.getElementById("rcjavPath").value.trim();
setNote("paths-summary", path ? `Using extension override: ${escapeHtml(path)}` : "Using native host default rc-jav path.", path ? "" : "info");
const profiles = readProfiles();
setNote("profiles-summary", profiles.length ? `${plural(profiles.length, "profile")} configured. Empty source/target lists inherit config.json defaults.` : "No profiles configured. Popup searches use rc-jav config.json defaults.", profiles.length ? "" : "info");
const settingCount = Object.keys(SETTINGS_SCHEMA).length;
setNote("backup-summary", `Backups include ${settingCount} recognized settings keys. Export before moving the extension or changing its ID.`, "info");
const deleteOn = document.getElementById("enableDelete").checked;
const mode = document.getElementById("deleteModePerm").checked ? "permanent delete" : "trash mode";
setNote("deletion-summary", deleteOn ? `Deletion is enabled in ${mode}. Popup confirmation still requires typing the exact filename.` : "Deletion is disabled. Popup delete controls stay hidden.", deleteOn ? "danger" : "");
}
function syncDeletionControls() {
const enabled = document.getElementById("enableDelete").checked;
for (const id of ["deleteModeTrash", "deleteModePerm", "trashDir"]) {
const el = document.getElementById(id);
if (el) el.disabled = !enabled;
}
for (const el of [document.getElementById("deleteModeTrashLbl")?.closest(".fieldset"), document.getElementById("trashDir")?.closest(".fieldset")]) {
if (el) el.classList.toggle("disabled-soft", !enabled);
}
}
document.getElementById("enableDelete").addEventListener("change", (event) => {
if (!event.target.checked) return;
event.target.checked = false;
syncDeletionControls();
updateSectionSummaries();
openModal("delete-enable-modal");
});
function closeDeleteEnableModal() {
closeModal("delete-enable-modal");
}
document.getElementById("delete-enable-modal-confirm").addEventListener("click", () => {
document.getElementById("enableDelete").checked = true;
closeDeleteEnableModal();
syncDeletionControls();
updateSectionSummaries();
});
for (const id of ["delete-enable-modal-close", "delete-enable-modal-cancel"]) {
document.getElementById(id).addEventListener("click", closeDeleteEnableModal);
}
document.getElementById("delete-enable-modal").addEventListener("click", (event) => {
if (event.target.id === "delete-enable-modal") closeDeleteEnableModal();
});
document.addEventListener("input", (e) => {
if (e.target && e.target.closest(".pane")) updateSectionSummaries();
});
document.addEventListener("change", (e) => {
if (e.target && e.target.closest(".pane")) {
if (e.target.id === "enableDelete") syncDeletionControls();
updateSectionSummaries();
}
});
// ---------- backup & restore ----------
document.getElementById("export-settings").addEventListener("click", async () => {
const { settings = {} } = await chrome.storage.sync.get("settings");
const payload = {
_meta: { app: "rclonex", exported_at: new Date().toISOString(), version: 1 },
settings,
};
const blob = new Blob([JSON.stringify(payload, null, 2)], { type: "application/json" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
const stamp = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19);
a.href = url;
a.download = `rclonex-settings-${stamp}.json`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
setBackupStatus("exported.", "ok");
});
document.getElementById("import-settings").addEventListener("click", () => {
document.getElementById("import-file").click();
});
// Allowlist of settings keys with their expected primitive types. Imports
// containing any other key are dropped silently; primitives must match.
// Nested objects (triggers, siteAdapters[].*) get a recursive shallow check.
const SETTINGS_SCHEMA = {
triggers: "object",
knownSitePatterns: "array",
quickMode: "boolean",
cacheStaleHours: "number",
scanPaused: "boolean",
rcjavPath: "string",
showOverlay: "boolean",
overlayPosition: "string",
overlayDuration: "number",
overlayGlow: "boolean",
overlayGlowColor: "string",
overlayGlowBlur: "number",
overlayGlowSpread: "number",
overlayGlowOpacity: "number",
noMatchOverlay: "boolean",
noMatchPosition: "string",
noMatchDuration: "number",
noMatchGlow: "boolean",
noMatchGlowColor: "string",
noMatchGlowBlur: "number",
noMatchGlowSpread: "number",
noMatchGlowOpacity: "number",
enableDelete: "boolean",
deleteMode: "string",
trashDir: "string",
idNormalizers: "array",
partPatterns: "array",
siteAdapters: "array",
profiles: "array",
activeProfile: "string",
};
function _typeOf(v) {
if (Array.isArray(v)) return "array";
if (v === null) return "null";
return typeof v;
}
function sanitizeImportedSettings(incoming) {
if (typeof incoming !== "object" || incoming === null || Array.isArray(incoming)) {
throw new Error("settings must be a JSON object");
}
const out = {};
const dropped = [];
for (const [k, v] of Object.entries(incoming)) {
const expected = SETTINGS_SCHEMA[k];
if (!expected) { dropped.push(k); continue; }
if (_typeOf(v) !== expected) { dropped.push(`${k}(wrong type)`); continue; }
out[k] = v;
}
return { sanitized: out, dropped };
}
let pendingImport = null;
function closeImportModal() {
pendingImport = null;
closeModal("import-modal");
}
function openImportModal(fileName, sanitized, dropped) {
pendingImport = { sanitized, dropped };
document.getElementById("import-modal-subtitle").textContent = fileName || "settings JSON";
document.getElementById("import-modal-body").innerHTML = `
Review this import before it replaces the current extension settings.
! Overwrite Current settings will be replaced by ${escapeHtml(Object.keys(sanitized).length)} imported value(s).
i Profiles ${escapeHtml(Array.isArray(sanitized.profiles) ? `${sanitized.profiles.length} profile(s) in this import` : "No profile list in this import")}
${sanitized.enableDelete ? "!" : "i"} Deletion ${escapeHtml(sanitized.enableDelete ? `Enabled in imported settings (${sanitized.deleteMode || "trash"} mode)` : "Not enabled by this import")}
${dropped.length ? `! Ignored keys ${escapeHtml(`${dropped.length}: ${dropped.slice(0, 8).join(", ")}${dropped.length > 8 ? "..." : ""}`)}
` : `✓ Schema All imported keys are recognized.
`}
`;
openModal("import-modal");
}
document.getElementById("import-file").addEventListener("change", async (e) => {
const file = e.target.files?.[0];
if (!file) return;
try {
const text = await file.text();
const data = JSON.parse(text);
const incoming = data.settings || data;
const { sanitized, dropped } = sanitizeImportedSettings(incoming);
if (Object.keys(sanitized).length === 0) {
throw new Error("no recognized settings keys in file");
}
openImportModal(file.name, sanitized, dropped);
} catch (err) {
setBackupStatus("import failed: " + err.message, "fail");
}
e.target.value = "";
});
document.getElementById("import-modal-confirm").addEventListener("click", async () => {
if (!pendingImport) return;
const { sanitized, dropped } = pendingImport;
try {
await chrome.storage.sync.set({ settings: sanitized });
chrome.runtime.sendMessage({ type: "settings-changed" });
closeImportModal();
setBackupStatus(`imported${dropped.length ? ` (${dropped.length} key(s) dropped)` : ""}. reloading...`, "ok");
setTimeout(() => location.reload(), 600);
} catch (err) {
setBackupStatus("import failed: " + (err.message || String(err)), "fail");
}
});
document.getElementById("import-modal-close").addEventListener("click", closeImportModal);
document.getElementById("import-modal-cancel").addEventListener("click", () => {
setBackupStatus("cancelled.", "warn");
closeImportModal();
});
document.getElementById("import-modal").addEventListener("click", (event) => {
if (event.target.id === "import-modal") closeImportModal();
});
function setBackupStatus(msg, kind) {
const el = document.getElementById("backup-status");
el.textContent = msg;
el.style.color = kind === "ok" ? "#afa" : kind === "fail" ? "#faa" : "#ffa";
setTimeout(() => { el.textContent = ""; }, 4000);
}
function appendListValue(textareaId, value) {
const el = document.getElementById(textareaId);
const parts = el.value.split(/[\n,]/).map((s) => s.trim()).filter(Boolean);
if (!parts.some((p) => p.toLowerCase() === value.toLowerCase())) parts.push(value);
el.value = parts.join("\n");
updateSectionSummaries();
}
document.getElementById("add-clearjav-site").addEventListener("click", () => {
appendListValue("knownSitePatterns", "clearjav.com");
document.getElementById("known-site-status").textContent = "added clearjav.com";
});
document.getElementById("add-current-site").addEventListener("click", async () => {
const status = document.getElementById("known-site-status");
status.textContent = "finding current site...";
const all = await chrome.tabs.query({});
const web = all.filter((t) => t.url && /^https?:/.test(t.url));
web.sort((a, b) => (b.lastAccessed || 0) - (a.lastAccessed || 0));
const tab = web[0];
if (!tab) {
status.textContent = "no http/https tab found";
return;
}
try {
const host = new URL(tab.url).hostname.replace(/^www\./, "");
appendListValue("knownSitePatterns", host);
status.textContent = `added ${host}`;
} catch {
status.textContent = "could not read active site";
}
});
// ---------- recent activity ----------
function fmtActivityWhen(ts) {
if (!ts) return "?";
const s = Math.max(0, Math.round((Date.now() - ts) / 1000));
if (s < 60) return `${s}s ago`;
if (s < 3600) return `${Math.round(s / 60)}m ago`;
if (s < 86400) return `${Math.round(s / 3600)}h ago`;
return `${Math.round(s / 86400)}d ago`;
}
let recentActivityEntries = [];
let activityFilter = "all";
function activityOutcomeView(outcome) {
if (outcome === "hit") return { label: "Match", cls: "hit" };
if (outcome === "miss") return { label: "No Match", cls: "miss" };
if (outcome === "no_id") return { label: "No ID", cls: "no-id" };
if (outcome === "paused") return { label: "Paused", cls: "paused" };
return { label: outcome === "error" ? "Error" : (outcome || "Unknown"), cls: "error" };
}
function activityMatchesFilter(entry) {
if (activityFilter === "all") return true;
if (activityFilter === "other") return !["hit", "miss", "no_id"].includes(entry.outcome);
return entry.outcome === activityFilter;
}
function updateActivityFilterButtons() {
document.querySelectorAll("#activity-filters .activity-filter").forEach((btn) => {
btn.classList.toggle("active", btn.dataset.activityFilter === activityFilter);
});
}
function renderActivity(entries = recentActivityEntries) {
recentActivityEntries = entries || [];
const out = document.getElementById("activity-results");
if (!recentActivityEntries.length) {
out.innerHTML = `no recent activity yet `;
return;
}
const visible = recentActivityEntries.filter(activityMatchesFilter).slice(0, 20);
if (!visible.length) {
out.innerHTML = `no recent activity for this filter `;
return;
}
out.innerHTML = visible.map((e) => {
const outcome = activityOutcomeView(e.outcome);
const id = e.id || "no ID";
const mode = e.mode ? ` · ${e.mode}` : "";
const timing = Number.isFinite(e.total_ms) ? ` · ${e.total_ms}ms total` : "";
const page = e.title || e.url || e.reason || "";
const reason = e.reason && e.reason !== page ? `${escapeHtml(e.reason)}
` : "";
return `
${escapeHtml(outcome.label)}
${escapeHtml(id)}
${escapeHtml(e.trigger || "page")} · ${escapeHtml(fmtActivityWhen(e.ts))}${escapeHtml(mode)}${escapeHtml(timing)}
${escapeHtml(page)}
${reason}
`;
}).join("");
}
async function refreshActivity() {
const out = document.getElementById("activity-results");
out.textContent = "loading activity...";
try {
const r = await chrome.runtime.sendMessage({ type: "recent-activity" });
if (!r || !r.ok) {
out.innerHTML = `error: ${escapeHtml(r?.error || "no response")}`;
return;
}
renderActivity(r.entries || []);
} catch (err) {
out.innerHTML = `error: ${escapeHtml(err.message || String(err))}`;
}
}
document.getElementById("activity-refresh").addEventListener("click", refreshActivity);
document.getElementById("activity-clear").addEventListener("click", async () => {
if (!confirm("Clear recent activity?")) return;
await chrome.runtime.sendMessage({ type: "clear-recent-activity" });
renderActivity([]);
});
document.getElementById("activity-filters").addEventListener("click", (e) => {
const btn = e.target.closest(".activity-filter");
if (!btn) return;
activityFilter = btn.dataset.activityFilter || "all";
updateActivityFilterButtons();
renderActivity();
});
updateActivityFilterButtons();
// ---------- search test bench ----------
function benchLookupRow(label, response) {
if (!response || !response.ok) {
const blocked = response?.paused ? "scanning paused" : response?.error || "no response";
return `x ${escapeHtml(label)} ${escapeHtml(blocked)}
`;
}
const query = (response.queries || [])[0] || {};
const hitCount = Number(query.hits || response.hits || 0);
const status = hitCount ? "ok" : "warn";
const sample = (query.structured || response.structured || [])
.slice(0, 2)
.map((row) => row.full_path || row.path || row.jav_id)
.filter(Boolean)
.join(" | ");
const timing = response.timings?.host_rcjav_ms ?? response.timings?.native_ms ?? "?";
const detail = `${hitCount} hit(s) · ${timing}ms${sample ? ` · ${sample}` : ` · ${query.no_match_title || response.no_match_title || "No library hit"}`}`;
return `${hitCount ? "OK" : "!"} ${escapeHtml(label)} ${escapeHtml(detail)}
`;
}
function benchLookupHits(response) {
if (!response || !response.ok) return null;
const query = (response.queries || [])[0] || {};
return Number(query.hits || response.hits || 0);
}
async function runSearchBench() {
const out = document.getElementById("search-bench-results");
const text = document.getElementById("search-bench-input").value.trim();
if (!text) {
out.innerHTML = `paste an ID or text that contains one `;
return;
}
out.textContent = "extracting ID...";
try {
const extraction = await chrome.runtime.sendMessage({
type: "test-id-text",
text,
normalizers: readNormalizers(),
});
if (!extraction || !extraction.ok) {
out.innerHTML = `extract failed: ${escapeHtml(extraction?.error || "no response")}`;
return;
}
const extracted = extraction.extracted || {};
const extractionRows = [
``,
`i Rule ${escapeHtml(extracted.source || "none")}${extracted.pattern ? ` · ${escapeHtml(extracted.pattern)}` : ""}${extracted.replacement ? ` -> ${escapeHtml(extracted.replacement)}` : ""}
`,
];
if (!extracted.id) {
out.innerHTML = extractionRows.join("") + `No lookup ran because the pasted sample did not extract an ID.
`;
return;
}
out.innerHTML = extractionRows.join("") + `Comparing LIVE and CACHE for ${escapeHtml(extracted.id)}...
`;
const [settings, live, cached] = await Promise.all([
chrome.runtime.sendMessage({ type: "get-settings" }),
chrome.runtime.sendMessage({ type: "bulk-query", queries: [extracted.id], quick: true }),
chrome.runtime.sendMessage({ type: "bulk-query", queries: [extracted.id], quick: false }),
]);
const liveHits = benchLookupHits(live);
const cacheHits = benchLookupHits(cached);
const mismatch = liveHits != null && cacheHits != null && liveHits !== cacheHits;
const activeProfile = settings?.activeProfile || "config.json defaults";
out.innerHTML = [
...extractionRows,
`i Profile ${escapeHtml(activeProfile)}
`,
benchLookupRow("LIVE", live),
benchLookupRow("CACHE", cached),
mismatch
? `! Mismatch LIVE and CACHE returned different hit counts. Check cache coverage/freshness and rebuild after ID-rule or library changes.
`
: `OK Compare LIVE and CACHE returned the same hit count.
`,
`Extraction uses the ID Rules currently shown on this page. Search lookups use saved host settings and the active profile.
`,
].join("");
} catch (err) {
out.innerHTML = `test failed: ${escapeHtml(err.message || String(err))}`;
}
}
document.getElementById("search-bench-run").addEventListener("click", runSearchBench);
document.getElementById("search-bench-clear").addEventListener("click", () => {
document.getElementById("search-bench-input").value = "";
document.getElementById("search-bench-results").innerHTML = "";
});
// ---------- bulk ID check ----------
function readBulkIds() {
return [...new Set(document.getElementById("bulk-id-input").value
.split(/[\s,]+/)
.map((x) => x.trim())
.filter(Boolean))];
}
function renderBulkResults(r) {
const out = document.getElementById("bulk-id-results");
if (!r || !r.ok) {
out.innerHTML = `error: ${escapeHtml(r?.error || "no response")}`;
return;
}
const rows = [
`Mode: ${escapeHtml(r.search_mode || "?")} · Queries: ${escapeHtml(r.query_count || 0)} · Hits: ${escapeHtml(r.hits || 0)} · Host: ${escapeHtml(r.timings?.host_rcjav_ms ?? "?")}ms
`,
];
for (const q of r.queries || []) {
const hit = q.hits > 0;
const sample = (q.structured || []).slice(0, 3).map((h) => h.full_path || h.path || h.jav_id).join(" | ");
rows.push(`
${hit ? "HIT" : "MISS"}
${escapeHtml(q.query || "?")} · ${escapeHtml(q.hits || 0)} hit(s)
${sample ? `
${escapeHtml(sample)}
` : `
${escapeHtml(q.no_match_title || "No library hit")}
`}
`);
}
out.innerHTML = rows.join("");
}
document.getElementById("bulk-id-run").addEventListener("click", async () => {
const out = document.getElementById("bulk-id-results");
const queries = readBulkIds();
if (!queries.length) {
out.innerHTML = `paste at least one ID `;
return;
}
out.textContent = `checking ${queries.length} ID(s)...`;
const r = await chrome.runtime.sendMessage({
type: "bulk-query",
queries,
quick: document.getElementById("quickMode").checked,
});
renderBulkResults(r);
});
document.getElementById("bulk-id-clear").addEventListener("click", () => {
document.getElementById("bulk-id-input").value = "";
document.getElementById("bulk-id-results").innerHTML = "";
});
// ---------- adapters ----------
function renderAdapters(list) {
const tbody = document.querySelector("#adapters tbody");
tbody.innerHTML = "";
for (const a of list) addAdapterRow(a.host || "", a.selector || "");
if (list.length === 0) addAdapterRow("", "");
}
function addAdapterRow(host, selector) {
const tbody = document.querySelector("#adapters tbody");
const tr = document.createElement("tr");
tr.innerHTML = `
× `;
tr.querySelector(".host").value = host;
tr.querySelector(".selector").value = selector;
tr.querySelector(".del").addEventListener("click", () => tr.remove());
tbody.appendChild(tr);
}
function readAdapters() {
const rows = document.querySelectorAll("#adapters tbody tr");
const out = [];
for (const tr of rows) {
const host = tr.querySelector(".host").value.trim();
const selector = tr.querySelector(".selector").value.trim();
if (host && selector) out.push({ host, selector });
}
return out;
}
document.getElementById("add-adapter").addEventListener("click", () => addAdapterRow("", ""));
document.getElementById("validate-adapters").addEventListener("click", () => {
const status = document.getElementById("picker-status");
const rows = [...document.querySelectorAll("#adapters tbody tr")];
const seen = new Set();
const issues = [];
for (const tr of rows) {
const host = tr.querySelector(".host").value.trim();
const selector = tr.querySelector(".selector").value.trim();
tr.style.outline = "";
if (!host && !selector) continue;
if (!host || !selector) {
issues.push("rows need both host and selector");
tr.style.outline = "1px solid #775";
continue;
}
const key = host.toLowerCase();
if (seen.has(key)) {
issues.push(`duplicate host: ${host}`);
tr.style.outline = "1px solid #775";
}
seen.add(key);
try { document.querySelector(selector); } catch {
issues.push(`invalid CSS selector for ${host}`);
tr.style.outline = "1px solid #775";
}
}
status.textContent = issues.length ? [...new Set(issues)].join("; ") : `${readAdapters().length} adapter row(s) look valid`;
updateSectionSummaries();
});
// ---------- ID normalizers ----------
function renderNormalizers(list) {
const tbody = document.querySelector("#normalizers tbody");
tbody.innerHTML = "";
for (const n of list) addNormalizerRow(n.re || "", n.fmt || "");
if (list.length === 0) addNormalizerRow("", "");
}
function addNormalizerRow(re, fmt) {
const tbody = document.querySelector("#normalizers tbody");
const tr = document.createElement("tr");
tr.innerHTML = `
× `;
tr.querySelector(".re").value = re;
tr.querySelector(".fmt").value = fmt;
tr.querySelector(".del").addEventListener("click", () => tr.remove());
tbody.appendChild(tr);
}
function readNormalizers() {
const rows = document.querySelectorAll("#normalizers tbody tr");
const out = [];
for (const tr of rows) {
const re = tr.querySelector(".re").value.trim();
const fmt = tr.querySelector(".fmt").value.trim();
if (re && fmt) out.push({ re, fmt });
}
return out;
}
document.getElementById("add-normalizer").addEventListener("click", () => addNormalizerRow("", ""));
document.getElementById("validate-normalizers").addEventListener("click", () => {
const status = document.getElementById("normalizer-status");
const rows = [...document.querySelectorAll("#normalizers tbody tr")];
const issues = [];
for (const tr of rows) {
tr.style.outline = "";
const re = tr.querySelector(".re").value.trim();
const fmt = tr.querySelector(".fmt").value.trim();
if (!re && !fmt) continue;
if (!re || !fmt) {
issues.push("rows need both regex and replacement");
tr.style.outline = "1px solid #775";
continue;
}
try { new RegExp(re, "i"); } catch (err) {
issues.push(`invalid regex: ${err.message}`);
tr.style.outline = "1px solid #775";
}
}
status.textContent = issues.length ? issues.join("; ") : `${readNormalizers().length} normalizer row(s) look valid`;
updateSectionSummaries();
});
// ---------- custom part detectors ----------
const PART_DETECTOR_SAMPLES = [
"KV-118 - Aiba Reika_PART1.mp4",
"KV-118 - Aiba Reika_PART2.mp4",
"KV-118 - Aiba Reika_PART3.mp4",
"KV-118_1.mp4",
"KV-118_2.mp4",
"KV-118-pt1.mp4",
"KV-118-part2.mp4",
"KV-118-cd1.mp4",
"KV-118-disc2.mp4",
"KV-118 (1).mp4",
"KV-118 (1 of 3).mp4",
"KV-118.1of3.mp4",
"KV-118-2 of 4.mp4",
"OFJE-195-1 [480p].mp4",
"OFJE-195-2 [480p].mp4",
"OFJE-195-3 [480p].mp4",
"KV-118_A.mp4",
"KV-118-B.mp4",
"KV-118A.mp4",
"KV-118 1.mp4",
"KV-118-P1.mp4",
"KV-118_P2.mp4",
"KV-118 Part 3.mp4",
"KV-118_EP1.mp4",
"KV-118 Episode 2.mp4",
"KV-118_Vol1.mp4",
"KV-118 Volume 2.mp4",
"KV-118_Scene1.mp4",
"KV-118_Side-A.mp4",
];
const BUILTIN_PART_DETECTORS = [
{ pattern: "[-_ ](?:pt|part|cd|disc)[-_ ]?(\\d+)$", note: "pt / part / cd / disc number" },
{ pattern: "\\s*\\((\\d+)(?:\\s*of\\s*\\d+)?\\)$", note: "parenthesized part number or X of Y" },
{ pattern: "[._ -](\\d+)\\s*of\\s*\\d+$", note: "X of Y suffix" },
{ pattern: "_(\\d{1,2})$", note: "underscore number" },
{ pattern: "-(\\d{1,2})$", note: "hyphen short part number" },
{ pattern: "[-_]([A-D])$", note: "lettered part with separator" },
{ pattern: "(?<=\\d)([A-D])$", note: "lettered part directly after ID" },
{ pattern: "\\s+(\\d{1,2})$", note: "trailing spaced number" },
];
function partDetectorStem(filename) {
return filename.replace(/\.[^.]+$/, "");
}
function partDetectorStemStages(filename) {
const raw = partDetectorStem(filename);
const resolutionClean = raw.replace(/\s*\[[^\]]*\]\s*$/, "").trim();
let actressClean = resolutionClean;
if (actressClean.includes(" - ")) actressClean = actressClean.slice(0, actressClean.indexOf(" - ")).trim();
const stages = [];
for (const [label, stem] of [
["raw stem", raw],
["after trailing metadata cleanup", resolutionClean],
["after actress cleanup", actressClean],
]) {
if (stem && !stages.some((stage) => stage.stem === stem)) stages.push({ label, stem });
}
return stages;
}
function partDetectorRegex(pattern) {
// Custom detectors are Python regexes, but the common detector subset is
// shared with browser RegExp. Preview the representative shapes here; rc-jav
// remains authoritative when the saved rule runs during scan/search.
return new RegExp(pattern, "i");
}
function builtinPartCoverage(filename) {
for (const detector of BUILTIN_PART_DETECTORS) {
try {
const re = partDetectorRegex(detector.pattern);
for (const stage of partDetectorStemStages(filename)) {
const match = stage.stem.match(re);
if (match && match[1]) return detector;
}
} catch {}
}
return null;
}
function updatePartDetectorFeedback(row) {
const feedback = row.querySelector(".part-detector-feedback");
const pattern = row.querySelector(".part-detector-pattern").value.trim();
if (!pattern) {
feedback.innerHTML = `Enter a detector regex. Capture group 1 should be the part token.`;
return;
}
let re;
try {
re = partDetectorRegex(pattern);
} catch (err) {
feedback.innerHTML = `Invalid preview regex: ${escapeHtml(err.message || String(err))}`;
return;
}
const matches = [];
let missingCapture = false;
for (const filename of PART_DETECTOR_SAMPLES) {
for (const stage of partDetectorStemStages(filename)) {
const match = stage.stem.match(re);
if (!match) continue;
if (!match[1]) missingCapture = true;
matches.push({ filename, part: match[1] || "?", stage: stage.label });
break;
}
}
if (!matches.length) {
feedback.innerHTML = `No representative sample matched. The rule may still be valid for a library-specific filename shape.`;
return;
}
const isBuiltin = row.classList.contains("builtin");
const covered = !isBuiltin ? matches.map((item) => ({ item, detector: builtinPartCoverage(item.filename) })) : [];
const alreadyCovered = covered.length && covered.every((entry) => entry.detector);
const coveredNote = alreadyCovered
? `These representative matches are already covered by built-in detector${new Set(covered.map((entry) => entry.detector.pattern)).size === 1 ? "" : "s"}.
`
: "";
feedback.innerHTML = [
`${missingCapture ? "Matched, but capture group 1 was missing for a sample." : `Matches ${matches.length} representative filename shape${matches.length === 1 ? "" : "s"}.`} `,
coveredNote,
...matches.slice(0, 4).map((item) => `${escapeHtml(item.filename)} -> part ${escapeHtml(item.part)} (${escapeHtml(item.stage)})
`),
matches.length > 4 ? `and ${escapeHtml(matches.length - 4)} more representative match(es)
` : "",
].filter(Boolean).join("");
}
function addPartDetectorRow(pattern = "", { builtin = false, note = "" } = {}) {
const list = document.getElementById(builtin ? "builtin-part-detectors" : "part-detectors");
const row = document.createElement("div");
row.className = "part-detector-row" + (builtin ? " builtin" : "");
row.innerHTML = `
${builtin ? `Built in ` : `x `}
${note ? `${escapeHtml(note)}
` : ""}
`;
row.querySelector(".part-detector-pattern").value = pattern;
if (!builtin) {
row.querySelector(".part-detector-pattern").addEventListener("input", () => {
updatePartDetectorFeedback(row);
updateSectionSummaries();
});
row.querySelector("button").addEventListener("click", () => {
row.remove();
if (!list.children.length) addPartDetectorRow("");
updateSectionSummaries();
});
}
list.appendChild(row);
updatePartDetectorFeedback(row);
return row;
}
function renderPartDetectors(patterns) {
const builtinList = document.getElementById("builtin-part-detectors");
const list = document.getElementById("part-detectors");
builtinList.innerHTML = "";
list.innerHTML = "";
for (const detector of BUILTIN_PART_DETECTORS) addPartDetectorRow(detector.pattern, { builtin: true, note: detector.note });
for (const pattern of patterns || []) addPartDetectorRow(pattern);
if (!list.children.length) addPartDetectorRow("");
}
function readPartDetectors() {
return [...document.querySelectorAll("#part-detectors .part-detector-pattern")]
.map((input) => input.value.trim())
.filter(Boolean);
}
document.getElementById("add-part-detector").addEventListener("click", () => {
addPartDetectorRow("").querySelector(".part-detector-pattern").focus();
});
// Tester
document.getElementById("norm-test-run").addEventListener("click", async () => {
const input = document.getElementById("norm-test-in").value;
const out = document.getElementById("norm-test-out");
out.textContent = "testing text...";
try {
const r = await chrome.runtime.sendMessage({
type: "test-id-text",
text: input,
normalizers: readNormalizers(),
});
if (!r || !r.ok) {
out.innerHTML = `error: ${escapeHtml(r?.error || "no response")}`;
return;
}
const e = r.extracted || {};
out.innerHTML = [
`ID: ${escapeHtml(e.id || "none")}
`,
`Rule: ${escapeHtml(e.source || "none")}
`,
e.pattern ? `Pattern: ${escapeHtml(e.pattern)}
` : "",
e.replacement ? `Replacement: ${escapeHtml(e.replacement)}
` : "",
e.raw ? `Raw: ${escapeHtml(e.raw)}
` : "",
].filter(Boolean).join("");
} catch (err) {
out.innerHTML = `error: ${escapeHtml(err.message || String(err))}`;
}
});
// ---------- element picker ----------
// Track the active picker poll so re-clicking "Pick Element" cancels the prior
// poll instead of leaving two racing on the same session-storage key.
let activePickerPoll = null;
window.addEventListener("pagehide", () => {
if (activePickerPoll != null) { clearInterval(activePickerPoll); activePickerPoll = null; }
});
async function startPicker() {
const status = document.getElementById("picker-status");
if (activePickerPoll != null) {
clearInterval(activePickerPoll);
activePickerPoll = null;
// Drop any stale prior result so the new poll doesn't see it.
await chrome.storage.session.remove("lastPickerResult");
}
status.textContent = "starting picker…";
const resp = await chrome.runtime.sendMessage({ type: "start-picker", from: "options" });
if (!resp || !resp.ok) {
status.textContent = "error: " + (resp?.error || "no response");
return;
}
status.textContent = `picker armed on: ${resp.url || "(unknown tab)"} — click an element, Esc to cancel`;
const start = Date.now();
const poll = setInterval(async () => {
const { lastPickerResult } = await chrome.storage.session.get("lastPickerResult");
if (lastPickerResult && lastPickerResult.ts > start) {
clearInterval(poll); activePickerPoll = null;
await chrome.storage.session.remove("lastPickerResult");
if (lastPickerResult.type === "picker-cancelled") {
status.textContent = "cancelled";
return;
}
const host = lastPickerResult.host || "";
const hostPattern = host ? host.replace(/^www\./, "") : "";
const selector = lastPickerResult.selector || "";
const rows = document.querySelectorAll("#adapters tbody tr");
let replaced = false;
for (const tr of rows) {
const existing = tr.querySelector(".host").value.trim().toLowerCase();
if (existing === hostPattern.toLowerCase()) {
tr.querySelector(".selector").value = selector;
replaced = true;
break;
}
}
if (!replaced) addAdapterRow(hostPattern, selector);
for (const tr of document.querySelectorAll("#adapters tbody tr")) {
const h = tr.querySelector(".host").value.trim();
const s = tr.querySelector(".selector").value.trim();
if (!h && !s) tr.remove();
}
status.textContent = `${replaced ? "updated" : "added"}: ${selector} (sample: "${(lastPickerResult.sample || "").slice(0, 60)}", detected: ${lastPickerResult.detectedId || "no ID"})`;
}
if (Date.now() - start > 120000) {
clearInterval(poll); activePickerPoll = null;
status.textContent = "timed out (2 min)";
}
}, 500);
activePickerPoll = poll;
}
document.getElementById("pick-element").addEventListener("click", startPicker);
document.getElementById("test-active-page").addEventListener("click", async () => {
const wrap = document.getElementById("adapter-test-result");
const out = document.getElementById("adapter-test-output");
wrap.style.display = "";
out.textContent = "testing active page...";
try {
const r = await chrome.runtime.sendMessage({
type: "test-active-page",
adapters: readAdapters(),
normalizers: readNormalizers(),
});
if (!r || !r.ok) {
out.innerHTML = `error: ${escapeHtml(r?.error || "no response")}`;
return;
}
const e = r.extracted || {};
const selected = e.selected || {};
const stage = (name, value) => `${escapeHtml(name)}: ${escapeHtml(value?.id || "none")} ${value?.raw ? ` · ${escapeHtml(value.raw)}` : ""}
`;
out.innerHTML = [
`ID: ${escapeHtml(e.id || "none")}
`,
`Source: ${escapeHtml(e.source || "none")}
`,
selected.adapter ? `Adapter: ${escapeHtml(selected.adapter)}
` : "",
selected.selector ? `Selector: ${escapeHtml(selected.selector)}
` : "",
selected.raw ? `Selected raw: ${escapeHtml(selected.raw)}
` : "",
`Stage trace
`,
stage("Adapter", e.stages?.adapter),
stage("Title", e.stages?.title),
stage("URL", e.stages?.url),
`URL: ${escapeHtml(r.tab?.url || "")}
`,
`Title: ${escapeHtml(r.tab?.title || "")}
`,
].filter(Boolean).join("");
} catch (err) {
out.innerHTML = `error: ${escapeHtml(err.message || String(err))}`;
}
});
// ---------- radio chips selected styling ----------
function syncRadioChips() {
const trashLbl = document.getElementById("deleteModeTrashLbl");
const permLbl = document.getElementById("deleteModePermLbl");
trashLbl.classList.toggle("selected", document.getElementById("deleteModeTrash").checked);
permLbl.classList.toggle("selected", document.getElementById("deleteModePerm").checked);
permLbl.classList.add("danger"); // permanent always carries the danger style; .selected.danger = red
}
document.getElementById("deleteModeTrash").addEventListener("change", syncRadioChips);
document.getElementById("deleteModePerm").addEventListener("change", syncRadioChips);
function syncOverlayPosChips() {
for (const r of document.querySelectorAll('input[name="overlayPosition"]')) {
r.parentElement.classList.toggle("selected", r.checked);
}
}
for (const r of document.querySelectorAll('input[name="overlayPosition"]')) {
r.addEventListener("change", () => { syncOverlayPosChips(); updateOverlayPreview(); });
}
// ---------- live overlay preview ----------
function hexToRgba(hex, a) {
const m = String(hex).match(/^#?([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/i);
if (!m) return `rgba(110,193,255,${a})`;
return `rgba(${parseInt(m[1], 16)},${parseInt(m[2], 16)},${parseInt(m[3], 16)},${a})`;
}
function updateOverlayPreview() {
const preview = document.getElementById("overlay-preview");
const stage = document.getElementById("overlay-preview-stage");
const posLabel = document.getElementById("overlay-preview-pos");
if (!preview) return;
const pos = (document.querySelector('input[name="overlayPosition"]:checked') || {}).value || "top-right";
const glow = document.getElementById("overlayGlow").checked;
const color = document.getElementById("overlayGlowColor").value || "#6ec1ff";
const blur = parseInt(document.getElementById("overlayGlowBlur").value, 10) || 0;
const spread = parseInt(document.getElementById("overlayGlowSpread").value, 10) || 0;
const opacity = parseFloat(document.getElementById("overlayGlowOpacity").value) || 0.35;
const dur = Math.max(1, parseInt(document.getElementById("overlayDuration").value, 10) || 5);
// Stage alignment hints at chosen position
stage.style.alignItems = pos.startsWith("top") ? "flex-start" : "flex-end";
stage.style.justifyContent = pos.endsWith("left") ? "flex-start" : "flex-end";
posLabel.textContent = `— ${pos} · ${dur}s · ${glow ? "glow on" : "no glow"}`;
// Reflect slider values
document.getElementById("overlayGlowBlurVal").textContent = `${blur} px`;
document.getElementById("overlayGlowSpreadVal").textContent = `${spread} px`;
document.getElementById("overlayGlowOpacityVal").textContent = opacity.toFixed(2);
preview.style.boxShadow = glow
? `0 6px 20px rgba(0,0,0,0.55), 0 0 ${blur}px ${spread}px ${hexToRgba(color, opacity)}`
: "0 6px 20px rgba(0,0,0,0.55)";
// Restart progress bar animation with current duration
const bar = document.getElementById("overlay-preview-bar");
bar.style.animation = "none";
// force reflow
void bar.offsetWidth;
bar.style.animation = `rxshrink-preview ${dur}s linear infinite`;
}
// Inject keyframes once
(function injectPreviewKeyframes() {
const s = document.createElement("style");
s.textContent = "@keyframes rxshrink-preview { from { transform: scaleX(1); } to { transform: scaleX(0); } }";
document.head.appendChild(s);
})();
document.getElementById("overlayGlow").addEventListener("change", updateOverlayPreview);
document.getElementById("overlayGlowColor").addEventListener("input", updateOverlayPreview);
document.getElementById("overlayDuration").addEventListener("input", updateOverlayPreview);
document.getElementById("overlayGlowBlur").addEventListener("input", updateOverlayPreview);
document.getElementById("overlayGlowSpread").addEventListener("input", updateOverlayPreview);
document.getElementById("overlayGlowOpacity").addEventListener("input", updateOverlayPreview);
document.getElementById("overlay-preview-replay").addEventListener("click", updateOverlayPreview);
// ---------- no-match overlay ----------
function syncNoMatchPosChips() {
for (const r of document.querySelectorAll('input[name="noMatchPosition"]')) {
r.parentElement.classList.toggle("selected", r.checked);
}
}
function updateMutualExclusion() {
// Disable the radio in noMatchPosition that matches the chosen overlayPosition.
const matchPos = (document.querySelector('input[name="overlayPosition"]:checked') || {}).value;
for (const r of document.querySelectorAll('input[name="noMatchPosition"]')) {
const same = r.value === matchPos;
r.disabled = same;
r.parentElement.style.opacity = same ? "0.4" : "";
r.parentElement.style.pointerEvents = same ? "none" : "";
if (same && r.checked) {
// Auto-switch to first non-disabled
r.checked = false;
const fallback = Array.from(document.querySelectorAll('input[name="noMatchPosition"]'))
.find((x) => x.value !== matchPos);
if (fallback) fallback.checked = true;
syncNoMatchPosChips();
updateNoMatchPreview();
}
}
// Same in reverse for overlayPosition (disable noMatch's selected one)
const nmPos = (document.querySelector('input[name="noMatchPosition"]:checked') || {}).value;
for (const r of document.querySelectorAll('input[name="overlayPosition"]')) {
const same = r.value === nmPos && document.getElementById("noMatchOverlay").checked;
r.disabled = same;
r.parentElement.style.opacity = same ? "0.4" : "";
r.parentElement.style.pointerEvents = same ? "none" : "";
}
}
function updateNoMatchPreview() {
const preview = document.getElementById("no-match-preview");
const stage = document.getElementById("no-match-preview-stage");
const posLabel = document.getElementById("no-match-preview-pos");
if (!preview) return;
const pos = (document.querySelector('input[name="noMatchPosition"]:checked') || {}).value || "top-right";
const glow = document.getElementById("noMatchGlow").checked;
const color = document.getElementById("noMatchGlowColor").value || "#ff6666";
const blur = parseInt(document.getElementById("noMatchGlowBlur").value, 10) || 0;
const spread = parseInt(document.getElementById("noMatchGlowSpread").value, 10) || 0;
const opacity = parseFloat(document.getElementById("noMatchGlowOpacity").value) || 0.35;
const dur = Math.max(1, parseInt(document.getElementById("noMatchDuration").value, 10) || 5);
stage.style.alignItems = pos.startsWith("top") ? "flex-start" : "flex-end";
stage.style.justifyContent = pos.endsWith("left") ? "flex-start" : "flex-end";
posLabel.textContent = `— ${pos} · ${dur}s · ${glow ? "glow on" : "no glow"}`;
document.getElementById("noMatchGlowBlurVal").textContent = `${blur} px`;
document.getElementById("noMatchGlowSpreadVal").textContent = `${spread} px`;
document.getElementById("noMatchGlowOpacityVal").textContent = opacity.toFixed(2);
preview.style.boxShadow = glow
? `0 6px 20px rgba(0,0,0,0.55), 0 0 ${blur}px ${spread}px ${hexToRgba(color, opacity)}`
: "0 6px 20px rgba(0,0,0,0.55)";
const bar = document.getElementById("no-match-preview-bar");
bar.style.animation = "none";
void bar.offsetWidth;
bar.style.animation = `rxshrink-preview ${dur}s linear infinite`;
}
for (const r of document.querySelectorAll('input[name="noMatchPosition"]')) {
r.addEventListener("change", () => { syncNoMatchPosChips(); updateNoMatchPreview(); updateMutualExclusion(); });
}
for (const id of ["noMatchOverlay", "noMatchGlow"]) {
document.getElementById(id).addEventListener("change", () => { updateNoMatchPreview(); updateMutualExclusion(); });
}
for (const id of ["noMatchGlowColor", "noMatchDuration", "noMatchGlowBlur", "noMatchGlowSpread", "noMatchGlowOpacity"]) {
document.getElementById(id).addEventListener("input", updateNoMatchPreview);
}
document.getElementById("no-match-preview-replay").addEventListener("click", updateNoMatchPreview);
// When match position changes, re-evaluate mutual exclusion
for (const r of document.querySelectorAll('input[name="overlayPosition"]')) {
r.addEventListener("change", updateMutualExclusion);
}
document.getElementById("no-match-reset").addEventListener("click", () => {
document.getElementById("noMatchOverlay").checked = false;
for (const r of document.querySelectorAll('input[name="noMatchPosition"]')) {
r.checked = r.value === "top-right";
}
syncNoMatchPosChips();
document.getElementById("noMatchDuration").value = 5;
document.getElementById("noMatchGlow").checked = false;
document.getElementById("noMatchGlowColor").value = "#ff6666";
document.getElementById("noMatchGlowBlur").value = 10;
document.getElementById("noMatchGlowSpread").value = 0;
document.getElementById("noMatchGlowOpacity").value = 0.35;
updateNoMatchPreview();
updateMutualExclusion();
updateSectionSummaries();
});
document.getElementById("overlay-reset").addEventListener("click", () => {
// Reset all overlay-related form fields to defaults.
document.getElementById("showOverlay").checked = true;
for (const r of document.querySelectorAll('input[name="overlayPosition"]')) {
r.checked = r.value === "top-right";
}
syncOverlayPosChips();
document.getElementById("overlayDuration").value = 5;
document.getElementById("overlayGlow").checked = false;
document.getElementById("overlayGlowColor").value = "#6ec1ff";
document.getElementById("overlayGlowBlur").value = 10;
document.getElementById("overlayGlowSpread").value = 0;
document.getElementById("overlayGlowOpacity").value = 0.35;
updateOverlayPreview();
updateSectionSummaries();
});
// ---------- diagnostics ----------
// Extension ID display + copy button (added when Transfer Assistant was deleted).
// Diagnostics is the canonical home for "what's my extension ID?" info now.
(() => {
const idEl = document.getElementById("diag-extension-id");
const copyBtn = document.getElementById("diag-copy-extension-id");
if (idEl) idEl.textContent = chrome.runtime.id;
if (copyBtn) {
copyBtn.addEventListener("click", async () => {
try {
await navigator.clipboard.writeText(chrome.runtime.id);
copyBtn.textContent = "Copied";
setTimeout(() => { copyBtn.textContent = "Copy ID"; }, 1200);
} catch (_) {
copyBtn.textContent = "Copy failed";
setTimeout(() => { copyBtn.textContent = "Copy ID"; }, 1200);
}
});
}
})();
document.getElementById("run-diag").addEventListener("click", (event) =>
keepActionViewport(event.currentTarget, runDiagnostics)
);
document.getElementById("host-status-run").addEventListener("click", (event) =>
keepActionViewport(event.currentTarget, runHostStatus)
);
document.getElementById("host-repair-run").addEventListener("click", (event) =>
keepActionViewport(event.currentTarget, runHostRepair)
);
document.getElementById("host-verify-run").addEventListener("click", (event) =>
keepActionViewport(event.currentTarget, runHostStatus)
);
document.getElementById("run-all-diag").addEventListener("click", (event) =>
keepActionViewport(event.currentTarget, async () => {
clearNativeRepairCard();
const runtime = await runDiagnostics();
if (runtime && runtime.nativeBlocked) {
renderBlockedByNativeIssue(document.getElementById("host-status-results"), "Host registration");
return;
}
await runHostStatus();
})
);
function renderDiagRows(out, checks, emptyLabel) {
out.innerHTML = "";
if (!checks || checks.length === 0) {
out.innerHTML = `! ${escapeHtml(emptyLabel)} no checks returned
`;
return;
}
const counts = checks.reduce((acc, c) => {
const status = c.status || "warn";
acc[status] = (acc[status] || 0) + 1;
return acc;
}, {});
const summary = document.createElement("div");
summary.className = "diag-row " + ((counts.fail || 0) ? "fail" : (counts.warn || 0) ? "warn" : "ok");
summary.innerHTML = `# summary ${checks.length} checks · ok ${counts.ok || 0} · info ${counts.info || 0} · warn ${counts.warn || 0} · fail ${counts.fail || 0} `;
out.appendChild(summary);
for (const c of checks) {
const row = document.createElement("div");
row.className = "diag-row " + (c.status || "warn");
const status = c.status || "warn";
const icon = status === "ok" ? "✓" : status === "info" ? "i" : status === "warn" ? "!" : "✗";
row.innerHTML = `${icon} ${escapeHtml(c.name)} ${formatDiagDetail(c.detail || "")} `;
out.appendChild(row);
}
}
function formatDiagDetail(detail) {
const text = String(detail || "");
if (!text) return "";
const shouldCollapse = text.length > 120 || text.includes("\n") || (text.match(/[;|]/g) || []).length > 2;
if (!shouldCollapse) return escapeHtml(text);
const first = text.split(/\r?\n/)[0].slice(0, 110);
return `${escapeHtml(first)}${text.length > first.length ? "…" : ""} ${escapeHtml(text)} `;
}
async function runDiagnostics() {
const out = document.getElementById("diag-results");
clearNativeRepairCard();
out.innerHTML = '… running… waiting for native host
';
try {
const r = await chrome.runtime.sendMessage({ type: "diagnostics" });
if (!r || !r.ok) {
await renderNativeMessagingFailure(r);
renderBlockedByNativeIssue(out, "Runtime diagnostics");
return { nativeBlocked: true };
}
renderDiagRows(out, r.checks || [], "runtime");
return { ok: true };
} catch (err) {
out.innerHTML = `✗ runtime ${escapeHtml(err.message || String(err))}
`;
return { ok: false };
}
}
async function runHostStatus() {
const out = document.getElementById("host-status-results");
clearNativeRepairCard();
out.innerHTML = '… checking… reading manifest and registry state
';
try {
const r = await chrome.runtime.sendMessage({ type: "host-status" });
if (!r || !r.ok) {
await renderNativeMessagingFailure(r);
renderBlockedByNativeIssue(out, "Native host checks");
return { nativeBlocked: true };
}
renderDiagRows(out, r.checks || [], "host status");
return { ok: true };
} catch (err) {
out.innerHTML = `✗ host status ${escapeHtml(err.message || String(err))}
`;
return { ok: false };
}
}
async function runHostRepair() {
const out = document.getElementById("host-status-results");
clearNativeRepairCard();
out.innerHTML = '… repairing… updating reachable native host manifest and user registration
';
try {
const r = await chrome.runtime.sendMessage({ type: "repair-host" });
if (!r || !r.ok) {
if (r?.error_kind) {
await renderNativeMessagingFailure(r);
renderBlockedByNativeIssue(out, "Registration repair");
} else {
out.innerHTML = `✗ Registration repair ${escapeHtml(r?.error || "repair failed")}
`;
}
return { ok: false };
}
const checks = r.verification?.checks || [];
renderDiagRows(out, checks, "repair verification");
renderCompletedNativeRepair(r);
return { ok: true };
} catch (err) {
out.innerHTML = `✗ Registration repair ${escapeHtml(err.message || String(err))}
`;
return { ok: false };
}
}
function clearNativeRepairCard() {
const card = document.getElementById("native-repair-card");
const out = document.getElementById("native-repair-results");
const title = document.getElementById("native-repair-title");
if (card) card.style.display = "none";
if (out) out.innerHTML = "";
if (title) title.textContent = "Native host setup";
}
function renderCompletedNativeRepair(response) {
const card = document.getElementById("native-repair-card");
const out = document.getElementById("native-repair-results");
if (!card || !out) return;
card.style.display = "";
const title = document.getElementById("native-repair-title");
if (title) title.textContent = "Registration repair completed";
const regs = (response.registrations || []).filter((x) => x.status === "ok").length;
out.innerHTML = `
✓ Repair applied ${escapeHtml(response.message || "native host registration repaired")}
i Manifest ${escapeHtml(response.manifest_path || "")}
i User registry ${escapeHtml(`${regs} HKCU registration entr${regs === 1 ? "y" : "ies"} updated`)}
! Restart required Fully close Brave, reopen it, reload the extension, then click Verify Registration. If Brave still blocks the host, run the registration steps shown by Diagnostics.
`;
}
function renderBlockedByNativeIssue(out, title) {
out.innerHTML = `i ${escapeHtml(title)} Blocked until this PC registers the native host for the current extension ID. Use the setup card above.
`;
}
async function getPackagedHostPaths() {
try {
const resp = await fetch(chrome.runtime.getURL("host/com.rcjav.host.json"));
if (!resp.ok) return {};
const manifest = await resp.json();
const bat = manifest.path || "";
const hostDir = bat.replace(/[\\/][^\\/]+$/, "");
return {
hostBat: bat,
hostDir,
registerBat: hostDir ? hostDir + "\\register-host.bat" : "",
installPs1: hostDir ? hostDir + "\\install-host.ps1" : "",
};
} catch {
return {};
}
}
async function renderNativeMessagingFailure(response) {
const card = document.getElementById("native-repair-card");
const out = document.getElementById("native-repair-results");
if (!card || !out) return;
card.style.display = "";
const title = document.getElementById("native-repair-title");
if (title) title.textContent = "Register host on this PC";
const error = response?.error || "no response";
const kind = response?.error_kind || (/forbidden/i.test(error) ? "forbidden" : "unknown");
const extensionId = response?.extension_id || chrome.runtime.id;
const paths = await getPackagedHostPaths();
const installCommand = paths.installPs1
? `pwsh -ExecutionPolicy Bypass -File "${paths.installPs1}" -ExtensionId ${extensionId}`
: `pwsh -ExecutionPolicy Bypass -File ".\\host\\install-host.ps1" -ExtensionId ${extensionId}`;
const registerCommand = paths.registerBat ? `"${paths.registerBat}"` : ".\\host\\register-host.bat";
let cause = "This extension cannot launch the native messaging host yet.";
let fix = "Register the host for this extension ID, fully restart Brave, then verify registration.";
if (kind === "forbidden") {
cause = "Brave found the native host, but this extension ID is not allowed to launch it on this PC.";
fix = "This usually happens after loading the extension on another PC or under a different extension ID.";
} else if (kind === "not_found") {
cause = "Brave could not find a registered native messaging host for com.rcjav.host on this PC.";
fix = "Run the registration script from the extension host folder.";
} else if (kind === "disconnected") {
cause = "The native host started and then disconnected or crashed.";
fix = "After registration is fixed, run Runtime diagnostics again to check Python, rc-jav, and rclone.";
} else if (kind === "timeout") {
cause = "The native host did not respond before the timeout.";
fix = "Restart Brave and check whether a scan or rclone command is stuck.";
}
out.innerHTML = `
! Setup required Native host registration must be fixed before cache, runtime, and host checks can run.
! Likely cause ${escapeHtml(cause)}
i Host message ${escapeHtml(error)}
→ Fix on this PC ${escapeHtml(fix)}
i Extension ID ${escapeHtml(extensionId)}
1 Run register-host
${escapeHtml(registerCommand)} ${escapeHtml(`Run ${registerCommand}\nWhen it asks for the extension ID, enter:\n${extensionId}\n\nPowerShell alternative:\n${installCommand}`)}
Copy Script Path Copy Extension ID Copy PowerShell Alternative
2 Restart Brave Close every Brave window/process, reopen Brave, then reload the extension.
3 Verify Verify Registration
`;
for (const btn of out.querySelectorAll("button[data-copy]")) {
btn.addEventListener("click", async () => {
await navigator.clipboard.writeText(btn.dataset.copy || "");
btn.textContent = "Copied";
setTimeout(() => { btn.textContent = btn.dataset.copyLabel || "Copy"; }, 1200);
});
}
for (const btn of out.querySelectorAll("button[data-verify-registration]")) {
btn.addEventListener("click", runHostStatus);
}
}
function escapeHtml(s) {
return String(s).replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[c]));
}
// ---------- profiles ----------
let _knownRemotes = []; // ["cq:", "gdrive:", ...] from rclone listremotes
let _cfgDefaults = { source: [], target: [] };
let _remotesLoaded = false;
async function fetchRemotes() {
const status = document.getElementById("profiles-status");
if (_remotesLoaded) return;
_remotesLoaded = true;
if (status) status.textContent = "loading remotes...";
try {
const r = await chrome.runtime.sendMessage({ type: "list-remotes" });
if (r && r.ok) {
_knownRemotes = r.remotes || [];
_cfgDefaults = { source: r.default_source || [], target: r.default_target || [] };
if (status) status.textContent = `${_knownRemotes.length} remote(s) loaded`;
// Re-render to populate selects now that we have data
const profiles = readProfiles();
renderProfiles(profiles);
updateSectionSummaries();
}
} catch (e) {
_remotesLoaded = false;
if (status) status.textContent = "failed to load remotes";
}
}
document.querySelector('.side .item[data-pane="profiles"]').addEventListener("click", fetchRemotes);
document.getElementById("load-remotes").addEventListener("click", () => {
_remotesLoaded = false;
fetchRemotes();
});
/**
* Build a remote picker widget.
* Shows: a dropdown of known remotes + an editable path suffix input +
* an Add button. Added remotes appear as chips below with × to remove.
* Falls back gracefully to a plain text input if no remotes loaded yet.
*/
function buildRemotePicker(container, values) {
container.innerHTML = "";
// --- selected list ---
const selectedList = document.createElement("div");
selectedList.className = "prof-selected-list";
selectedList.style.cssText = "margin-bottom:6px;";
container.appendChild(selectedList);
function addChip(path) {
const chip = document.createElement("div");
chip.className = "prof-chip";
chip.style.cssText = "display:flex;align-items:center;gap:6px;margin-bottom:4px;";
// Editable path so user can adjust subpath after picking
const inp = document.createElement("input");
inp.type = "text";
inp.value = path;
inp.className = "prof-chip-input";
inp.style.cssText = "flex:1;font-family:Consolas,monospace;font-size:12px;";
const rm = document.createElement("button");
rm.type = "button";
rm.textContent = "×";
rm.title = "Remove";
rm.style.cssText = "background:#511;border:1px solid #722;color:#faa;border-radius:3px;padding:0 7px;cursor:pointer;font-size:14px;line-height:1;";
rm.addEventListener("click", () => chip.remove());
chip.appendChild(inp);
chip.appendChild(rm);
selectedList.appendChild(chip);
}
// Pre-populate with existing values
for (const v of (values || [])) addChip(v);
// --- add row: select + optional subpath + Add button ---
const addRow = document.createElement("div");
addRow.style.cssText = "display:flex;gap:6px;align-items:center;flex-wrap:wrap;";
const sel = document.createElement("select");
sel.style.cssText = "background:#0d0d0d;color:#ddd;border:1px solid #2a2a2a;border-radius:4px;padding:5px 8px;font-family:Consolas,monospace;font-size:12px;min-width:130px;";
if (_knownRemotes.length) {
for (const r of _knownRemotes) {
const opt = document.createElement("option");
opt.value = r;
opt.textContent = r;
sel.appendChild(opt);
}
} else {
const opt = document.createElement("option");
opt.value = "";
opt.textContent = "(loading…)";
sel.appendChild(opt);
}
const subpathInp = document.createElement("input");
subpathInp.type = "text";
subpathInp.placeholder = "optional/subpath";
subpathInp.style.cssText = "flex:1;min-width:120px;font-family:Consolas,monospace;font-size:12px;";
subpathInp.title = "Append a subpath to narrow the remote, e.g. JAV/ClearJAV";
const addBtn = document.createElement("button");
addBtn.type = "button";
addBtn.textContent = "+ Add";
addBtn.style.cssText = "padding:5px 12px;font-size:12px;white-space:nowrap;";
addBtn.addEventListener("click", () => {
const base = sel.value.trim();
if (!base) return;
const sub = subpathInp.value.trim().replace(/^\//, "");
const full = sub ? base + sub : base;
addChip(full);
subpathInp.value = "";
});
// Also allow typing a fully custom path
const customInp = document.createElement("input");
customInp.type = "text";
customInp.placeholder = "or type full path (e.g. cq:JAV/ClearJAV)";
customInp.style.cssText = "flex:1;min-width:160px;font-family:Consolas,monospace;font-size:12px;margin-top:4px;";
const customBtn = document.createElement("button");
customBtn.type = "button";
customBtn.textContent = "+ Add";
customBtn.style.cssText = "padding:5px 12px;font-size:12px;margin-top:4px;white-space:nowrap;";
customBtn.addEventListener("click", () => {
const v = customInp.value.trim();
if (v) { addChip(v); customInp.value = ""; }
});
customInp.addEventListener("keydown", (e) => { if (e.key === "Enter") { e.preventDefault(); customBtn.click(); } });
addRow.appendChild(sel);
addRow.appendChild(subpathInp);
addRow.appendChild(addBtn);
container.appendChild(addRow);
const customRow = document.createElement("div");
customRow.style.cssText = "display:flex;gap:6px;align-items:center;flex-wrap:wrap;margin-top:4px;";
customRow.appendChild(customInp);
customRow.appendChild(customBtn);
container.appendChild(customRow);
}
function readRemoteGroup(container) {
return [...container.querySelectorAll(".prof-chip-input")]
.map(i => i.value.trim()).filter(Boolean);
}
function describeProfileRoots(label, values, defaults) {
const roots = (values || []).filter(Boolean);
if (roots.length) return `${label}: ${roots.join(", ")}`;
if ((defaults || []).length) return `${label}: config default ${defaults.join(", ")}`;
return `${label}: config.json default`;
}
function cloneProfile(profile) {
return {
name: (profile?.name || "").trim(),
source: [...(profile?.source || [])].filter(Boolean),
target: [...(profile?.target || [])].filter(Boolean),
};
}
function buildProfileRow(profile) {
const p = cloneProfile(profile);
const row = document.createElement("div");
row.className = "profile-card prof-row";
row._profile = p;
const detail = document.createElement("div");
detail.innerHTML = `
${escapeHtml(p.name)}
${escapeHtml(describeProfileRoots("Source", p.source, _cfgDefaults.source))} ${escapeHtml(describeProfileRoots("Target", p.target, _cfgDefaults.target))}
`;
const actions = document.createElement("div");
actions.className = "actions";
const editBtn = document.createElement("button");
editBtn.type = "button";
editBtn.textContent = "Edit";
editBtn.addEventListener("click", () => openProfileModal(row));
const delBtn = document.createElement("button");
delBtn.type = "button";
delBtn.className = "danger";
delBtn.textContent = "Remove";
delBtn.addEventListener("click", () => {
row.remove();
updateSectionSummaries();
});
actions.appendChild(editBtn);
actions.appendChild(delBtn);
row.appendChild(detail);
row.appendChild(actions);
return row;
}
function renderProfiles(profiles) {
const list = document.getElementById("profiles-list");
list.innerHTML = "";
if (!profiles.length) {
const msg = document.createElement("div");
msg.style.cssText = "color:#666;font-size:12px;font-style:italic;margin-bottom:8px;";
msg.textContent = "No profiles defined. Searches use rc-jav's config.json defaults.";
list.appendChild(msg);
return;
}
for (const p of profiles) list.appendChild(buildProfileRow(p));
}
function readProfiles() {
return [...document.querySelectorAll("#profiles-list .prof-row")]
.map((row) => cloneProfile(row._profile))
.filter((profile) => profile.name);
}
let editingProfileRow = null;
function setProfileModalDefaultsNote() {
const src = _cfgDefaults.source.length ? _cfgDefaults.source.join(", ") : "config.json default_source";
const tgt = _cfgDefaults.target.length ? _cfgDefaults.target.join(", ") : "config.json default_target";
document.getElementById("profile-modal-status").textContent = `Empty remote lists inherit source ${src} and target ${tgt}.`;
}
async function openProfileModal(row = null) {
editingProfileRow = row;
await fetchRemotes();
const profile = cloneProfile(row?._profile);
document.getElementById("profile-modal-title").textContent = row ? "Edit Library Profile" : "Add Library Profile";
document.getElementById("profile-modal-name").value = profile.name;
buildRemotePicker(document.getElementById("profile-modal-source"), profile.source);
buildRemotePicker(document.getElementById("profile-modal-target"), profile.target);
setProfileModalDefaultsNote();
openModal("profile-modal");
document.getElementById("profile-modal-name").focus();
}
function closeProfileModal() {
editingProfileRow = null;
closeModal("profile-modal");
}
document.getElementById("add-profile").addEventListener("click", () => openProfileModal());
document.getElementById("profile-modal-save").addEventListener("click", () => {
const name = document.getElementById("profile-modal-name").value.trim();
const status = document.getElementById("profile-modal-status");
if (!name) {
status.textContent = "Profile name is required.";
return;
}
const profile = {
name,
source: readRemoteGroup(document.getElementById("profile-modal-source")),
target: readRemoteGroup(document.getElementById("profile-modal-target")),
};
const list = document.getElementById("profiles-list");
if (editingProfileRow) {
editingProfileRow.replaceWith(buildProfileRow(profile));
} else {
list.querySelector("div[style*='italic']")?.remove();
list.appendChild(buildProfileRow(profile));
}
closeProfileModal();
updateSectionSummaries();
});
for (const id of ["profile-modal-close", "profile-modal-cancel"]) {
document.getElementById(id).addEventListener("click", closeProfileModal);
}
document.getElementById("profile-modal").addEventListener("click", (event) => {
if (event.target.id === "profile-modal") closeProfileModal();
});
// ---------- paths ----------
document.getElementById("clear-rcjav-path").addEventListener("click", () => {
document.getElementById("rcjavPath").value = "";
document.getElementById("path-check-output").textContent = "using host default";
updateSectionSummaries();
});
document.getElementById("check-rcjav-path").addEventListener("click", async () => {
const out = document.getElementById("path-check-output");
out.textContent = "checking...";
chrome.runtime.sendMessage({ type: "ping-host", rcjavPath: document.getElementById("rcjavPath").value.trim() }, (r) => {
if (chrome.runtime.lastError) {
out.textContent = chrome.runtime.lastError.message;
return;
}
if (!r || !r.ok) {
out.textContent = r?.error || "no response";
return;
}
out.textContent = r.rc_jav_exists ? `ok: ${r.rc_jav}` : `not found: ${r.rc_jav}`;
updateSectionSummaries();
});
});
(async () => {
const { optionsActivePane, pendingNativeSetupIssue } = await chrome.storage.local.get(["optionsActivePane", "pendingNativeSetupIssue"]);
activatePane(optionsActivePane || "triggers");
await load();
if (pendingNativeSetupIssue) {
activatePane("diagnostics");
await renderNativeMessagingFailure(pendingNativeSetupIssue);
await chrome.storage.local.remove("pendingNativeSetupIssue");
}
refreshActivity();
})();