Step 6c: extract Diagnostics + Profiles + Rules Editors from options.js
Final options.js split. Three new files:
options-diagnostics.js 245 lines
options-profiles.js 265 lines
options-rules-editors.js 328 lines (adapters + ID normalizers
+ custom part detectors)
options.js: 1852 → 1014 lines (838 extracted, ~45% reduction).
Script-tag order in options.html now (load order matters for
top-level let bindings shared across files, e.g. _configuredScanRoots):
options-cache.js
options-dupe-review.js
options-library-issues.js
options-diagnostics.js
options-profiles.js
options-rules-editors.js
options.js (entry: IIFE bottom, escapeHtml, overlay previews,
element picker, paths)
The picker, overlay-preview, and no-match overlay code stays in
options.js — those are tightly intertwined with multiple settings
panes and not worth further splitting today.
node --check passes on each file individually and on the concatenated
load-order stream. Line count of concat (3144) matches the pre-split
sum exactly.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,245 @@
|
|||||||
|
// ---------- 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 = `<div class="diag-row warn"><span class="icon">!</span><span class="name">${escapeHtml(emptyLabel)}</span><span class="detail">no checks returned</span></div>`;
|
||||||
|
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 = `<span class="icon">#</span><span class="name">summary</span><span class="detail">${checks.length} checks · ok ${counts.ok || 0} · info ${counts.info || 0} · warn ${counts.warn || 0} · fail ${counts.fail || 0}</span>`;
|
||||||
|
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 = `<span class="icon">${icon}</span><span class="name">${escapeHtml(c.name)}</span><span class="detail">${formatDiagDetail(c.detail || "")}</span>`;
|
||||||
|
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 `<details><summary>${escapeHtml(first)}${text.length > first.length ? "…" : ""}</summary><pre>${escapeHtml(text)}</pre></details>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function runDiagnostics() {
|
||||||
|
const out = document.getElementById("diag-results");
|
||||||
|
clearNativeRepairCard();
|
||||||
|
out.innerHTML = '<div class="diag-row warn"><span class="icon">…</span><span class="name">running…</span><span class="detail">waiting for native host</span></div>';
|
||||||
|
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 = `<div class="diag-row fail"><span class="icon">✗</span><span class="name">runtime</span><span class="detail">${escapeHtml(err.message || String(err))}</span></div>`;
|
||||||
|
return { ok: false };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function runHostStatus() {
|
||||||
|
const out = document.getElementById("host-status-results");
|
||||||
|
clearNativeRepairCard();
|
||||||
|
out.innerHTML = '<div class="diag-row warn"><span class="icon">…</span><span class="name">checking…</span><span class="detail">reading manifest and registry state</span></div>';
|
||||||
|
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 = `<div class="diag-row fail"><span class="icon">✗</span><span class="name">host status</span><span class="detail">${escapeHtml(err.message || String(err))}</span></div>`;
|
||||||
|
return { ok: false };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function runHostRepair() {
|
||||||
|
const out = document.getElementById("host-status-results");
|
||||||
|
clearNativeRepairCard();
|
||||||
|
out.innerHTML = '<div class="diag-row warn"><span class="icon">…</span><span class="name">repairing…</span><span class="detail">updating reachable native host manifest and user registration</span></div>';
|
||||||
|
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 = `<div class="diag-row fail"><span class="icon">✗</span><span class="name">Registration repair</span><span class="detail">${escapeHtml(r?.error || "repair failed")}</span></div>`;
|
||||||
|
}
|
||||||
|
return { ok: false };
|
||||||
|
}
|
||||||
|
const checks = r.verification?.checks || [];
|
||||||
|
renderDiagRows(out, checks, "repair verification");
|
||||||
|
renderCompletedNativeRepair(r);
|
||||||
|
return { ok: true };
|
||||||
|
} catch (err) {
|
||||||
|
out.innerHTML = `<div class="diag-row fail"><span class="icon">✗</span><span class="name">Registration repair</span><span class="detail">${escapeHtml(err.message || String(err))}</span></div>`;
|
||||||
|
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 = `
|
||||||
|
<div class="diag-row ok"><span class="icon">✓</span><span class="name">Repair applied</span><span class="detail">${escapeHtml(response.message || "native host registration repaired")}</span></div>
|
||||||
|
<div class="diag-row info"><span class="icon">i</span><span class="name">Manifest</span><span class="detail">${escapeHtml(response.manifest_path || "")}</span></div>
|
||||||
|
<div class="diag-row info"><span class="icon">i</span><span class="name">User registry</span><span class="detail">${escapeHtml(`${regs} HKCU registration entr${regs === 1 ? "y" : "ies"} updated`)}</span></div>
|
||||||
|
<div class="diag-row warn"><span class="icon">!</span><span class="name">Restart required</span><span class="detail">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.</span></div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderBlockedByNativeIssue(out, title) {
|
||||||
|
out.innerHTML = `<div class="diag-row info"><span class="icon">i</span><span class="name">${escapeHtml(title)}</span><span class="detail">Blocked until this PC registers the native host for the current extension ID. Use the setup card above.</span></div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 = `
|
||||||
|
<div class="diag-row warn"><span class="icon">!</span><span class="name">Setup required</span><span class="detail">Native host registration must be fixed before cache, runtime, and host checks can run.</span></div>
|
||||||
|
<div class="diag-row warn"><span class="icon">!</span><span class="name">Likely cause</span><span class="detail">${escapeHtml(cause)}</span></div>
|
||||||
|
<div class="diag-row info"><span class="icon">i</span><span class="name">Host message</span><span class="detail">${escapeHtml(error)}</span></div>
|
||||||
|
<div class="diag-row ok"><span class="icon">→</span><span class="name">Fix on this PC</span><span class="detail">${escapeHtml(fix)}</span></div>
|
||||||
|
<div class="diag-row info"><span class="icon">i</span><span class="name">Extension ID</span><span class="detail">${escapeHtml(extensionId)}</span></div>
|
||||||
|
<div class="diag-row info"><span class="icon">1</span><span class="name">Run register-host</span><span class="detail">
|
||||||
|
<details open><summary>${escapeHtml(registerCommand)}</summary><pre>${escapeHtml(`Run ${registerCommand}\nWhen it asks for the extension ID, enter:\n${extensionId}\n\nPowerShell alternative:\n${installCommand}`)}</pre></details>
|
||||||
|
<span class="diag-action"><button type="button" data-copy="${escapeHtml(registerCommand)}" data-copy-label="Copy Script Path">Copy Script Path</button><button type="button" data-copy="${escapeHtml(extensionId)}" data-copy-label="Copy Extension ID">Copy Extension ID</button><button type="button" data-copy="${escapeHtml(installCommand)}" data-copy-label="Copy PowerShell Alternative">Copy PowerShell Alternative</button></span>
|
||||||
|
</span></div>
|
||||||
|
<div class="diag-row info"><span class="icon">2</span><span class="name">Restart Brave</span><span class="detail">Close every Brave window/process, reopen Brave, then reload the extension.</span></div>
|
||||||
|
<div class="diag-row info"><span class="icon">3</span><span class="name">Verify</span><span class="detail"><span class="diag-action"><button type="button" data-verify-registration>Verify Registration</button></span></span></div>
|
||||||
|
`;
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,265 @@
|
|||||||
|
// ---------- 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 <select> 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 = `
|
||||||
|
<div class="name">${escapeHtml(p.name)}</div>
|
||||||
|
<div class="roots">${escapeHtml(describeProfileRoots("Source", p.source, _cfgDefaults.source))}<br>${escapeHtml(describeProfileRoots("Target", p.target, _cfgDefaults.target))}</div>
|
||||||
|
`;
|
||||||
|
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();
|
||||||
|
});
|
||||||
|
|
||||||
@@ -0,0 +1,328 @@
|
|||||||
|
// ---------- 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 = `
|
||||||
|
<td><input type="text" class="host" placeholder="clearjav.com"></td>
|
||||||
|
<td><input type="text" class="selector" placeholder=".some-class"></td>
|
||||||
|
<td><button class="del" type="button">×</button></td>`;
|
||||||
|
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 = `
|
||||||
|
<td><input type="text" class="re" placeholder="\\b1pondo-?(\\d{4,})-?(\\d{2,})\\b"></td>
|
||||||
|
<td><input type="text" class="fmt" placeholder="1pondo-$1-$2"></td>
|
||||||
|
<td><button class="del" type="button">×</button></td>`;
|
||||||
|
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 = `<span class="warn">Enter a detector regex.</span> Capture group 1 should be the part token.`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let re;
|
||||||
|
try {
|
||||||
|
re = partDetectorRegex(pattern);
|
||||||
|
} catch (err) {
|
||||||
|
feedback.innerHTML = `<span class="fail">Invalid preview regex:</span> ${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 = `<span class="warn">No representative sample matched.</span> 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
|
||||||
|
? `<div class="warn">These representative matches are already covered by built-in detector${new Set(covered.map((entry) => entry.detector.pattern)).size === 1 ? "" : "s"}.</div>`
|
||||||
|
: "";
|
||||||
|
feedback.innerHTML = [
|
||||||
|
`<span class="${missingCapture ? "warn" : "ok"}">${missingCapture ? "Matched, but capture group 1 was missing for a sample." : `Matches ${matches.length} representative filename shape${matches.length === 1 ? "" : "s"}.`}</span>`,
|
||||||
|
coveredNote,
|
||||||
|
...matches.slice(0, 4).map((item) => `<div class="part-detector-match">${escapeHtml(item.filename)} -> part ${escapeHtml(item.part)} <span style="color:#777;">(${escapeHtml(item.stage)})</span></div>`),
|
||||||
|
matches.length > 4 ? `<div>and ${escapeHtml(matches.length - 4)} more representative match(es)</div>` : "",
|
||||||
|
].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 = `
|
||||||
|
<div class="part-detector-head">
|
||||||
|
<input type="text" class="part-detector-pattern" placeholder="_PART(\\d+)$"${builtin ? " readonly" : ""}>
|
||||||
|
${builtin ? `<span class="part-detector-kind">Built in</span>` : `<button type="button" title="Remove detector">x</button>`}
|
||||||
|
</div>
|
||||||
|
${note ? `<div class="muted" style="margin-top:5px;">${escapeHtml(note)}</div>` : ""}
|
||||||
|
<div class="part-detector-feedback"></div>
|
||||||
|
`;
|
||||||
|
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 = `<span style="color:#faa;">error:</span> ${escapeHtml(r?.error || "no response")}`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const e = r.extracted || {};
|
||||||
|
out.innerHTML = [
|
||||||
|
`<div><span style="color:#777;">ID:</span> <span style="color:${e.id ? "#afa" : "#faa"};">${escapeHtml(e.id || "none")}</span></div>`,
|
||||||
|
`<div><span style="color:#777;">Rule:</span> ${escapeHtml(e.source || "none")}</div>`,
|
||||||
|
e.pattern ? `<div><span style="color:#777;">Pattern:</span> ${escapeHtml(e.pattern)}</div>` : "",
|
||||||
|
e.replacement ? `<div><span style="color:#777;">Replacement:</span> ${escapeHtml(e.replacement)}</div>` : "",
|
||||||
|
e.raw ? `<div><span style="color:#777;">Raw:</span> ${escapeHtml(e.raw)}</div>` : "",
|
||||||
|
].filter(Boolean).join("");
|
||||||
|
} catch (err) {
|
||||||
|
out.innerHTML = `<span style="color:#faa;">error:</span> ${escapeHtml(err.message || String(err))}`;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
@@ -794,6 +794,9 @@
|
|||||||
<script src="options-cache.js"></script>
|
<script src="options-cache.js"></script>
|
||||||
<script src="options-dupe-review.js"></script>
|
<script src="options-dupe-review.js"></script>
|
||||||
<script src="options-library-issues.js"></script>
|
<script src="options-library-issues.js"></script>
|
||||||
|
<script src="options-diagnostics.js"></script>
|
||||||
|
<script src="options-profiles.js"></script>
|
||||||
|
<script src="options-rules-editors.js"></script>
|
||||||
<script src="options.js"></script>
|
<script src="options.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
-838
@@ -674,334 +674,6 @@ document.getElementById("search-bench-clear").addEventListener("click", () => {
|
|||||||
document.getElementById("search-bench-results").innerHTML = "";
|
document.getElementById("search-bench-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 = `
|
|
||||||
<td><input type="text" class="host" placeholder="clearjav.com"></td>
|
|
||||||
<td><input type="text" class="selector" placeholder=".some-class"></td>
|
|
||||||
<td><button class="del" type="button">×</button></td>`;
|
|
||||||
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 = `
|
|
||||||
<td><input type="text" class="re" placeholder="\\b1pondo-?(\\d{4,})-?(\\d{2,})\\b"></td>
|
|
||||||
<td><input type="text" class="fmt" placeholder="1pondo-$1-$2"></td>
|
|
||||||
<td><button class="del" type="button">×</button></td>`;
|
|
||||||
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 = `<span class="warn">Enter a detector regex.</span> Capture group 1 should be the part token.`;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let re;
|
|
||||||
try {
|
|
||||||
re = partDetectorRegex(pattern);
|
|
||||||
} catch (err) {
|
|
||||||
feedback.innerHTML = `<span class="fail">Invalid preview regex:</span> ${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 = `<span class="warn">No representative sample matched.</span> 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
|
|
||||||
? `<div class="warn">These representative matches are already covered by built-in detector${new Set(covered.map((entry) => entry.detector.pattern)).size === 1 ? "" : "s"}.</div>`
|
|
||||||
: "";
|
|
||||||
feedback.innerHTML = [
|
|
||||||
`<span class="${missingCapture ? "warn" : "ok"}">${missingCapture ? "Matched, but capture group 1 was missing for a sample." : `Matches ${matches.length} representative filename shape${matches.length === 1 ? "" : "s"}.`}</span>`,
|
|
||||||
coveredNote,
|
|
||||||
...matches.slice(0, 4).map((item) => `<div class="part-detector-match">${escapeHtml(item.filename)} -> part ${escapeHtml(item.part)} <span style="color:#777;">(${escapeHtml(item.stage)})</span></div>`),
|
|
||||||
matches.length > 4 ? `<div>and ${escapeHtml(matches.length - 4)} more representative match(es)</div>` : "",
|
|
||||||
].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 = `
|
|
||||||
<div class="part-detector-head">
|
|
||||||
<input type="text" class="part-detector-pattern" placeholder="_PART(\\d+)$"${builtin ? " readonly" : ""}>
|
|
||||||
${builtin ? `<span class="part-detector-kind">Built in</span>` : `<button type="button" title="Remove detector">x</button>`}
|
|
||||||
</div>
|
|
||||||
${note ? `<div class="muted" style="margin-top:5px;">${escapeHtml(note)}</div>` : ""}
|
|
||||||
<div class="part-detector-feedback"></div>
|
|
||||||
`;
|
|
||||||
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 = `<span style="color:#faa;">error:</span> ${escapeHtml(r?.error || "no response")}`;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const e = r.extracted || {};
|
|
||||||
out.innerHTML = [
|
|
||||||
`<div><span style="color:#777;">ID:</span> <span style="color:${e.id ? "#afa" : "#faa"};">${escapeHtml(e.id || "none")}</span></div>`,
|
|
||||||
`<div><span style="color:#777;">Rule:</span> ${escapeHtml(e.source || "none")}</div>`,
|
|
||||||
e.pattern ? `<div><span style="color:#777;">Pattern:</span> ${escapeHtml(e.pattern)}</div>` : "",
|
|
||||||
e.replacement ? `<div><span style="color:#777;">Replacement:</span> ${escapeHtml(e.replacement)}</div>` : "",
|
|
||||||
e.raw ? `<div><span style="color:#777;">Raw:</span> ${escapeHtml(e.raw)}</div>` : "",
|
|
||||||
].filter(Boolean).join("");
|
|
||||||
} catch (err) {
|
|
||||||
out.innerHTML = `<span style="color:#faa;">error:</span> ${escapeHtml(err.message || String(err))}`;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// ---------- element picker ----------
|
// ---------- element picker ----------
|
||||||
|
|
||||||
// Track the active picker poll so re-clicking "Pick Element" cancels the prior
|
// Track the active picker poll so re-clicking "Pick Element" cancels the prior
|
||||||
@@ -1300,520 +972,10 @@ document.getElementById("overlay-reset").addEventListener("click", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
// ---------- 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 = `<div class="diag-row warn"><span class="icon">!</span><span class="name">${escapeHtml(emptyLabel)}</span><span class="detail">no checks returned</span></div>`;
|
|
||||||
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 = `<span class="icon">#</span><span class="name">summary</span><span class="detail">${checks.length} checks · ok ${counts.ok || 0} · info ${counts.info || 0} · warn ${counts.warn || 0} · fail ${counts.fail || 0}</span>`;
|
|
||||||
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 = `<span class="icon">${icon}</span><span class="name">${escapeHtml(c.name)}</span><span class="detail">${formatDiagDetail(c.detail || "")}</span>`;
|
|
||||||
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 `<details><summary>${escapeHtml(first)}${text.length > first.length ? "…" : ""}</summary><pre>${escapeHtml(text)}</pre></details>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function runDiagnostics() {
|
|
||||||
const out = document.getElementById("diag-results");
|
|
||||||
clearNativeRepairCard();
|
|
||||||
out.innerHTML = '<div class="diag-row warn"><span class="icon">…</span><span class="name">running…</span><span class="detail">waiting for native host</span></div>';
|
|
||||||
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 = `<div class="diag-row fail"><span class="icon">✗</span><span class="name">runtime</span><span class="detail">${escapeHtml(err.message || String(err))}</span></div>`;
|
|
||||||
return { ok: false };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function runHostStatus() {
|
|
||||||
const out = document.getElementById("host-status-results");
|
|
||||||
clearNativeRepairCard();
|
|
||||||
out.innerHTML = '<div class="diag-row warn"><span class="icon">…</span><span class="name">checking…</span><span class="detail">reading manifest and registry state</span></div>';
|
|
||||||
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 = `<div class="diag-row fail"><span class="icon">✗</span><span class="name">host status</span><span class="detail">${escapeHtml(err.message || String(err))}</span></div>`;
|
|
||||||
return { ok: false };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function runHostRepair() {
|
|
||||||
const out = document.getElementById("host-status-results");
|
|
||||||
clearNativeRepairCard();
|
|
||||||
out.innerHTML = '<div class="diag-row warn"><span class="icon">…</span><span class="name">repairing…</span><span class="detail">updating reachable native host manifest and user registration</span></div>';
|
|
||||||
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 = `<div class="diag-row fail"><span class="icon">✗</span><span class="name">Registration repair</span><span class="detail">${escapeHtml(r?.error || "repair failed")}</span></div>`;
|
|
||||||
}
|
|
||||||
return { ok: false };
|
|
||||||
}
|
|
||||||
const checks = r.verification?.checks || [];
|
|
||||||
renderDiagRows(out, checks, "repair verification");
|
|
||||||
renderCompletedNativeRepair(r);
|
|
||||||
return { ok: true };
|
|
||||||
} catch (err) {
|
|
||||||
out.innerHTML = `<div class="diag-row fail"><span class="icon">✗</span><span class="name">Registration repair</span><span class="detail">${escapeHtml(err.message || String(err))}</span></div>`;
|
|
||||||
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 = `
|
|
||||||
<div class="diag-row ok"><span class="icon">✓</span><span class="name">Repair applied</span><span class="detail">${escapeHtml(response.message || "native host registration repaired")}</span></div>
|
|
||||||
<div class="diag-row info"><span class="icon">i</span><span class="name">Manifest</span><span class="detail">${escapeHtml(response.manifest_path || "")}</span></div>
|
|
||||||
<div class="diag-row info"><span class="icon">i</span><span class="name">User registry</span><span class="detail">${escapeHtml(`${regs} HKCU registration entr${regs === 1 ? "y" : "ies"} updated`)}</span></div>
|
|
||||||
<div class="diag-row warn"><span class="icon">!</span><span class="name">Restart required</span><span class="detail">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.</span></div>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderBlockedByNativeIssue(out, title) {
|
|
||||||
out.innerHTML = `<div class="diag-row info"><span class="icon">i</span><span class="name">${escapeHtml(title)}</span><span class="detail">Blocked until this PC registers the native host for the current extension ID. Use the setup card above.</span></div>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
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 = `
|
|
||||||
<div class="diag-row warn"><span class="icon">!</span><span class="name">Setup required</span><span class="detail">Native host registration must be fixed before cache, runtime, and host checks can run.</span></div>
|
|
||||||
<div class="diag-row warn"><span class="icon">!</span><span class="name">Likely cause</span><span class="detail">${escapeHtml(cause)}</span></div>
|
|
||||||
<div class="diag-row info"><span class="icon">i</span><span class="name">Host message</span><span class="detail">${escapeHtml(error)}</span></div>
|
|
||||||
<div class="diag-row ok"><span class="icon">→</span><span class="name">Fix on this PC</span><span class="detail">${escapeHtml(fix)}</span></div>
|
|
||||||
<div class="diag-row info"><span class="icon">i</span><span class="name">Extension ID</span><span class="detail">${escapeHtml(extensionId)}</span></div>
|
|
||||||
<div class="diag-row info"><span class="icon">1</span><span class="name">Run register-host</span><span class="detail">
|
|
||||||
<details open><summary>${escapeHtml(registerCommand)}</summary><pre>${escapeHtml(`Run ${registerCommand}\nWhen it asks for the extension ID, enter:\n${extensionId}\n\nPowerShell alternative:\n${installCommand}`)}</pre></details>
|
|
||||||
<span class="diag-action"><button type="button" data-copy="${escapeHtml(registerCommand)}" data-copy-label="Copy Script Path">Copy Script Path</button><button type="button" data-copy="${escapeHtml(extensionId)}" data-copy-label="Copy Extension ID">Copy Extension ID</button><button type="button" data-copy="${escapeHtml(installCommand)}" data-copy-label="Copy PowerShell Alternative">Copy PowerShell Alternative</button></span>
|
|
||||||
</span></div>
|
|
||||||
<div class="diag-row info"><span class="icon">2</span><span class="name">Restart Brave</span><span class="detail">Close every Brave window/process, reopen Brave, then reload the extension.</span></div>
|
|
||||||
<div class="diag-row info"><span class="icon">3</span><span class="name">Verify</span><span class="detail"><span class="diag-action"><button type="button" data-verify-registration>Verify Registration</button></span></span></div>
|
|
||||||
`;
|
|
||||||
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) {
|
function escapeHtml(s) {
|
||||||
return String(s).replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[c]));
|
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 <select> 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 = `
|
|
||||||
<div class="name">${escapeHtml(p.name)}</div>
|
|
||||||
<div class="roots">${escapeHtml(describeProfileRoots("Source", p.source, _cfgDefaults.source))}<br>${escapeHtml(describeProfileRoots("Target", p.target, _cfgDefaults.target))}</div>
|
|
||||||
`;
|
|
||||||
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 ----------
|
// ---------- paths ----------
|
||||||
|
|
||||||
document.getElementById("clear-rcjav-path").addEventListener("click", () => {
|
document.getElementById("clear-rcjav-path").addEventListener("click", () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user