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,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();
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user