Files
ext-rclone-jav/options-profiles.js
T
admin d0a2def788 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>
2026-05-23 11:17:55 +02:00

266 lines
10 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// ---------- 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();
});