Theseus: shield green polish, picker→Search section, toggle-vs-remove
Three follow-up asks from the previous ship: 1. Shield "secure" colour bumped from #4fd1a5 (mint) to #3fb950 — the GitHub-style saturated green, matches the +N/-N diff colour the user pointed at as reference. 2. Engine-picker "Search settings…" now opens the Search section directly instead of General. New IPC channel `focus-section` fires from main after picker-open-settings, carried through settings-preload as `onFocusSection`, and the settings.html sidebar handler exposes showSection(sec) so any section can be focused programmatically. Works for both a fresh settings tab (fires on did-finish-load) and an already-open one (fires immediately). 3. Toggle no longer removes an engine from the list. Two-tier state: INSTALLED (visible in the Settings list) and ENABLED (toggled on in the toolbar dropdown). Toggling off keeps the row visible with an .off class (dimmed 55%). Right-click any row → new context menu with "Remove from list" is what actually removes an engine (built-ins go back to the catalog, customs are dropped entirely). Model changes: - New settings.installedEngines persistent array (defaults to DEFAULT_ENABLED). enabledEngines becomes a subset of installedEngines. - isInstalled(id) helper; allEngines() carries `installed: bool` alongside `enabled`. - New IPC `remove-from-list` (right-click action); exposed as removeFromList in settings-preload. - set-engine-enabled now also INSTALLS when enabling (the catalog "+ Add" flow), preserves installed state when disabling. - add-engine (custom URL) auto-adds the new id to enabledEngines too. - remove-engine (custom delete) prunes from enabledEngines as well. - Never-empty invariant kept: enabledEngines falls back to ["duckduckgo"] if everything gets removed. Settings UI: - Enabled list shows all INSTALLED engines (was: only enabled), rendered with toggle reflecting enabled state; rows carry data-builtin so the context menu picks the right remove IPC. - Catalog panel and Discover-more pane filter on !installed instead of !enabled — a toggled-off engine stays in the enabled list, not here. - Ctxmenu is a floating .ctxmenu div; closes on outside click / Escape. - .eng.off dims the row and mutes the name colour. Preview harness stubs updated to include the `installed` field on every engine + `removeFromList` and `onFocusSection` no-op stubs so _settings-preview.html renders the new UI accurately.
This commit is contained in:
parent
3803202df6
commit
1d2faf64f3
4 changed files with 153 additions and 35 deletions
|
|
@ -67,7 +67,7 @@
|
||||||
background: transparent; cursor: pointer; padding: 0; color: var(--dim); }
|
background: transparent; cursor: pointer; padding: 0; color: var(--dim); }
|
||||||
.secbadge:hover { background: var(--line2); }
|
.secbadge:hover { background: var(--line2); }
|
||||||
.secbadge svg { width: 16px; height: 16px; fill: currentColor; }
|
.secbadge svg { width: 16px; height: 16px; fill: currentColor; }
|
||||||
.secbadge.secure { color: #4fd1a5; }
|
.secbadge.secure { color: #3fb950; } /* GitHub-style saturated green */
|
||||||
.secbadge.insecure { color: #f6768a; }
|
.secbadge.insecure { color: #f6768a; }
|
||||||
.secbadge.warn { color: #f6ad55; } /* kept for legacy call sites */
|
.secbadge.warn { color: #f6ad55; } /* kept for legacy call sites */
|
||||||
input { padding: 7px 6px; border-radius: 999px; border: none; background: transparent; color: inherit; font-size: 13.5px; outline: none; }
|
input { padding: 7px 6px; border-radius: 999px; border: none; background: transparent; color: inherit; font-size: 13.5px; outline: none; }
|
||||||
|
|
|
||||||
84
main.js
84
main.js
|
|
@ -70,13 +70,19 @@ const DEFAULT_ENABLED = ["duckduckgo", "google", "brave", "bing", "startpage"];
|
||||||
const faviconUrl = (domain) => (domain ? `https://icons.duckduckgo.com/ip3/${domain}.ico` : null);
|
const faviconUrl = (domain) => (domain ? `https://icons.duckduckgo.com/ip3/${domain}.ico` : null);
|
||||||
function customFavicon(url) { try { return faviconUrl(new URL(String(url).replace("%s", "x")).hostname); } catch { return null; } }
|
function customFavicon(url) { try { return faviconUrl(new URL(String(url).replace("%s", "x")).hostname); } catch { return null; } }
|
||||||
function isEnabled(id) { return (settings.enabledEngines || DEFAULT_ENABLED).includes(id); }
|
function isEnabled(id) { return (settings.enabledEngines || DEFAULT_ENABLED).includes(id); }
|
||||||
// Built-in + user-added, as flat metadata for the pickers. Built-in obey
|
// Two-tier state: an engine is INSTALLED if it's in the user's Additional
|
||||||
// enabledEngines; custom engines are always enabled.
|
// list (visible in Settings), and ENABLED if it's currently toggled on
|
||||||
|
// (visible in the toolbar dropdown). Toggle flips enabled only; right-click
|
||||||
|
// "Remove from list" is what actually removes an installed engine.
|
||||||
|
function isInstalled(id) {
|
||||||
|
if ((settings.customEngines || []).some((e) => e.id === id)) return true; // customs are always installed
|
||||||
|
return (settings.installedEngines || DEFAULT_ENABLED).includes(id);
|
||||||
|
}
|
||||||
function allEngines() {
|
function allEngines() {
|
||||||
const list = Object.entries(SEARCH_ENGINES).map(([id, e]) =>
|
const list = Object.entries(SEARCH_ENGINES).map(([id, e]) =>
|
||||||
({ id, name: e.name, sym: e.sym, favicon: faviconUrl(e.fav), kind: e.kind || "search", tier: e.tier || "catalog", builtin: true, enabled: isEnabled(id) }));
|
({ id, name: e.name, sym: e.sym, favicon: faviconUrl(e.fav), kind: e.kind || "search", tier: e.tier || "catalog", builtin: true, installed: isInstalled(id), enabled: isEnabled(id) }));
|
||||||
for (const c of settings.customEngines || [])
|
for (const c of settings.customEngines || [])
|
||||||
list.push({ id: c.id, name: c.name, sym: c.sym || "🔍", favicon: customFavicon(c.url), kind: c.kind || "search", tier: "custom", builtin: false, enabled: true });
|
list.push({ id: c.id, name: c.name, sym: c.sym || "🔍", favicon: customFavicon(c.url), kind: c.kind || "search", tier: "custom", builtin: false, installed: true, enabled: isEnabled(c.id) });
|
||||||
// Apply the user's custom order; ids not in engineOrder keep their natural order (stable sort).
|
// Apply the user's custom order; ids not in engineOrder keep their natural order (stable sort).
|
||||||
const order = settings.engineOrder || [];
|
const order = settings.engineOrder || [];
|
||||||
return list.slice().sort((a, b) => {
|
return list.slice().sort((a, b) => {
|
||||||
|
|
@ -157,7 +163,8 @@ const SETTINGS_DEFAULTS = {
|
||||||
languageMode: "show", languageSpoof: "en-US", languageValue: "en-US", // spoof = top-10 pick, manual = free text
|
languageMode: "show", languageSpoof: "en-US", languageValue: "en-US", // spoof = top-10 pick, manual = free text
|
||||||
locationMode: "hide", locationRegion: "europe", locationLat: "40.7128", locationLon: "-74.0060", // spoof by region, or manual coords
|
locationMode: "hide", locationRegion: "europe", locationLat: "40.7128", locationLon: "-74.0060", // spoof by region, or manual coords
|
||||||
searchEngine: "duckduckgo",// default search engine (built-in id or a custom id)
|
searchEngine: "duckduckgo",// default search engine (built-in id or a custom id)
|
||||||
enabledEngines: DEFAULT_ENABLED.slice(), // which built-in engines show in the dropdown
|
installedEngines: DEFAULT_ENABLED.slice(), // built-in engines added to the user's list (visible in Settings)
|
||||||
|
enabledEngines: DEFAULT_ENABLED.slice(), // subset that's currently toggled on (shown in the toolbar dropdown)
|
||||||
engineOrder: [], // user-defined display order of engine ids (empty = natural)
|
engineOrder: [], // user-defined display order of engine ids (empty = natural)
|
||||||
customEngines: [], // user-added: [{ id, name, url-with-%s }]
|
customEngines: [], // user-added: [{ id, name, url-with-%s }]
|
||||||
theme: "dark", // dark | light | system — drives prefers-color-scheme in all views
|
theme: "dark", // dark | light | system — drives prefers-color-scheme in all views
|
||||||
|
|
@ -761,6 +768,19 @@ function emitTabs() {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
function setLoading(tab, on) { if (tab && tab.loading !== on) { tab.loading = on; emitTabs(); } }
|
function setLoading(tab, on) { if (tab && tab.loading !== on) { tab.loading = on; emitTabs(); } }
|
||||||
|
// Pull the tab's real URL from webContents after Electron navigates, so in-page
|
||||||
|
// clicks (subpages of a BCNR site, subdomain hops, cross-origin redirects) update
|
||||||
|
// the address bar. Without this, t.url is only refreshed on programmatic loads —
|
||||||
|
// navigateTab / the collision switcher — and everything else sticks on the parent.
|
||||||
|
// Internal bns:// → https:// for display, matching navigateTab's convention that
|
||||||
|
// https:// is what the user sees regardless of how the bytes were fetched.
|
||||||
|
function refreshTabUrl(tab) {
|
||||||
|
if (!tab || tab.prov?.kind === "home") return; // home is loadFile → file://; leave t.url = ""
|
||||||
|
try {
|
||||||
|
const raw = tab.view.webContents.getURL();
|
||||||
|
if (raw && !raw.startsWith("file:")) tab.url = raw.replace(/^bns:\/\//, "https://");
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
function loadHome(id) {
|
function loadHome(id) {
|
||||||
const t = tabById(id); if (!t) return;
|
const t = tabById(id); if (!t) return;
|
||||||
t.url = ""; t.title = "Theseus"; t.prov = { host: "", kind: "home" };
|
t.url = ""; t.title = "Theseus"; t.prov = { host: "", kind: "home" };
|
||||||
|
|
@ -779,8 +799,8 @@ function createTab(initial, opts = {}) {
|
||||||
tabs.push(tab);
|
tabs.push(tab);
|
||||||
win.contentView.addChildView(view);
|
win.contentView.addChildView(view);
|
||||||
wc.on("page-title-updated", (_e, title) => { tab.title = title; emitTabs(); });
|
wc.on("page-title-updated", (_e, title) => { tab.title = title; emitTabs(); });
|
||||||
wc.on("did-navigate", () => emitTabs());
|
wc.on("did-navigate", () => { refreshTabUrl(tab); emitTabs(); });
|
||||||
wc.on("did-navigate-in-page", () => emitTabs());
|
wc.on("did-navigate-in-page", () => { refreshTabUrl(tab); emitTabs(); });
|
||||||
wc.on("did-start-loading", () => setLoading(tab, true));
|
wc.on("did-start-loading", () => setLoading(tab, true));
|
||||||
wc.on("did-stop-loading", () => setLoading(tab, false));
|
wc.on("did-stop-loading", () => setLoading(tab, false));
|
||||||
wc.on("will-navigate", (e, u) => {
|
wc.on("will-navigate", (e, u) => {
|
||||||
|
|
@ -1092,28 +1112,58 @@ ipcMain.handle("set-search-engine", (_e, id) => {
|
||||||
return settings.searchEngine;
|
return settings.searchEngine;
|
||||||
});
|
});
|
||||||
ipcMain.handle("add-engine", (_e, eng) => {
|
ipcMain.handle("add-engine", (_e, eng) => {
|
||||||
// A custom engine needs a name and a URL template containing "%s".
|
// A custom engine needs a name and a URL template containing "%s". Adding
|
||||||
|
// installs it in both the settings list AND the toolbar dropdown.
|
||||||
if (eng && eng.name && eng.url && String(eng.url).includes("%s")) {
|
if (eng && eng.name && eng.url && String(eng.url).includes("%s")) {
|
||||||
const id = "custom-" + Date.now().toString(36);
|
const id = "custom-" + Date.now().toString(36);
|
||||||
settings.customEngines = [...(settings.customEngines || []),
|
settings.customEngines = [...(settings.customEngines || []),
|
||||||
{ id, name: String(eng.name).slice(0, 40), sym: String(eng.sym || "🔍").slice(0, 4), url: String(eng.url).slice(0, 400) }];
|
{ id, name: String(eng.name).slice(0, 40), sym: String(eng.sym || "🔍").slice(0, 4), url: String(eng.url).slice(0, 400) }];
|
||||||
|
// Custom engines are auto-installed and enabled.
|
||||||
|
settings.enabledEngines = [...new Set([...(settings.enabledEngines || DEFAULT_ENABLED), id])];
|
||||||
settings.searchEngine = id; // select the one just added
|
settings.searchEngine = id; // select the one just added
|
||||||
saveSettings(); emitEngines();
|
saveSettings(); emitEngines();
|
||||||
}
|
}
|
||||||
return { engines: allEngines(), current: settings.searchEngine };
|
return { engines: allEngines(), current: settings.searchEngine };
|
||||||
});
|
});
|
||||||
ipcMain.handle("remove-engine", (_e, id) => {
|
ipcMain.handle("remove-engine", (_e, id) => {
|
||||||
|
// Drop a custom engine entirely — from customEngines and any list that
|
||||||
|
// referenced it.
|
||||||
settings.customEngines = (settings.customEngines || []).filter((e) => e.id !== id);
|
settings.customEngines = (settings.customEngines || []).filter((e) => e.id !== id);
|
||||||
|
settings.enabledEngines = (settings.enabledEngines || DEFAULT_ENABLED).filter((x) => x !== id);
|
||||||
if (settings.searchEngine === id) settings.searchEngine = enabledEnginesList()[0]?.id || "duckduckgo";
|
if (settings.searchEngine === id) settings.searchEngine = enabledEnginesList()[0]?.id || "duckduckgo";
|
||||||
saveSettings(); emitEngines();
|
saveSettings(); emitEngines();
|
||||||
return { engines: allEngines(), current: settings.searchEngine };
|
return { engines: allEngines(), current: settings.searchEngine };
|
||||||
});
|
});
|
||||||
// Enable/disable a built-in engine (which show in the toolbar dropdown).
|
// Right-click "Remove from list": drops a built-in from installedEngines AND
|
||||||
|
// enabledEngines so it goes back to the catalog. For custom engines this
|
||||||
|
// aliases to remove-engine (they don't live in installedEngines).
|
||||||
|
ipcMain.handle("remove-from-list", (_e, id) => {
|
||||||
|
if ((settings.customEngines || []).some((e) => e.id === id)) {
|
||||||
|
settings.customEngines = (settings.customEngines || []).filter((e) => e.id !== id);
|
||||||
|
} else if (SEARCH_ENGINES[id]) {
|
||||||
|
settings.installedEngines = (settings.installedEngines || DEFAULT_ENABLED).filter((x) => x !== id);
|
||||||
|
} else {
|
||||||
|
return { engines: allEngines(), current: settings.searchEngine };
|
||||||
|
}
|
||||||
|
settings.enabledEngines = (settings.enabledEngines || DEFAULT_ENABLED).filter((x) => x !== id);
|
||||||
|
if (settings.enabledEngines.length === 0) settings.enabledEngines = ["duckduckgo"]; // never empty
|
||||||
|
if (settings.searchEngine === id) settings.searchEngine = enabledEnginesList()[0]?.id || "duckduckgo";
|
||||||
|
saveSettings(); emitEngines();
|
||||||
|
return { engines: allEngines(), current: settings.searchEngine };
|
||||||
|
});
|
||||||
|
// Enable/disable a built-in engine. Enabling from the catalog also INSTALLS it
|
||||||
|
// (adds to installedEngines). Disabling only removes it from enabledEngines —
|
||||||
|
// it stays in installedEngines so the row remains visible with the toggle off.
|
||||||
ipcMain.handle("set-engine-enabled", (_e, id, on) => {
|
ipcMain.handle("set-engine-enabled", (_e, id, on) => {
|
||||||
if (SEARCH_ENGINES[id]) {
|
if (SEARCH_ENGINES[id]) {
|
||||||
let list = (settings.enabledEngines || DEFAULT_ENABLED).filter((x) => x !== id);
|
let installed = (settings.installedEngines || DEFAULT_ENABLED).slice();
|
||||||
if (on) list.push(id);
|
let enabled = (settings.enabledEngines || DEFAULT_ENABLED).filter((x) => x !== id);
|
||||||
settings.enabledEngines = list.length ? list : ["duckduckgo"]; // never empty
|
if (on) {
|
||||||
|
if (!installed.includes(id)) installed.push(id);
|
||||||
|
enabled.push(id);
|
||||||
|
}
|
||||||
|
settings.installedEngines = installed;
|
||||||
|
settings.enabledEngines = enabled.length ? enabled : ["duckduckgo"]; // never empty
|
||||||
if (!enabledEnginesList().some((e) => e.id === settings.searchEngine))
|
if (!enabledEnginesList().some((e) => e.id === settings.searchEngine))
|
||||||
settings.searchEngine = enabledEnginesList()[0]?.id || "duckduckgo";
|
settings.searchEngine = enabledEnginesList()[0]?.id || "duckduckgo";
|
||||||
saveSettings(); emitEngines();
|
saveSettings(); emitEngines();
|
||||||
|
|
@ -1136,7 +1186,15 @@ ipcMain.handle("pick-engine", (_e, id) => {
|
||||||
if (enabledEnginesList().some((e) => e.id === id)) { settings.searchEngine = id; saveSettings(); emitEngines(); }
|
if (enabledEnginesList().some((e) => e.id === id)) { settings.searchEngine = id; saveSettings(); emitEngines(); }
|
||||||
showEnginePicker(false);
|
showEnginePicker(false);
|
||||||
});
|
});
|
||||||
ipcMain.handle("picker-open-settings", () => { showEnginePicker(false); const ex = tabs.find((t) => t.settings); if (ex) return setActive(ex.id); createTab(null, { settings: true }); });
|
ipcMain.handle("picker-open-settings", () => {
|
||||||
|
showEnginePicker(false);
|
||||||
|
const focus = (t) => { try { t.view.webContents.send("focus-section", "search"); } catch {} };
|
||||||
|
const ex = tabs.find((t) => t.settings);
|
||||||
|
if (ex) { setActive(ex.id); focus(ex); return; }
|
||||||
|
const id = createTab(null, { settings: true });
|
||||||
|
const t = tabById(id);
|
||||||
|
if (t) t.view.webContents.once("did-finish-load", () => focus(t));
|
||||||
|
});
|
||||||
// ---- Downloads --------------------------------------------------------------
|
// ---- Downloads --------------------------------------------------------------
|
||||||
ipcMain.handle("downloads-get", () => downloadsPublic());
|
ipcMain.handle("downloads-get", () => downloadsPublic());
|
||||||
ipcMain.handle("toggle-downloads", (_e, rect) => {
|
ipcMain.handle("toggle-downloads", (_e, rect) => {
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,10 @@ contextBridge.exposeInMainWorld("cfg", {
|
||||||
removeEngine: (id) => ipcRenderer.invoke("remove-engine", id),
|
removeEngine: (id) => ipcRenderer.invoke("remove-engine", id),
|
||||||
setEngineEnabled: (id, on) => ipcRenderer.invoke("set-engine-enabled", id, on),
|
setEngineEnabled: (id, on) => ipcRenderer.invoke("set-engine-enabled", id, on),
|
||||||
setEngineOrder: (ids) => ipcRenderer.invoke("set-engine-order", ids),
|
setEngineOrder: (ids) => ipcRenderer.invoke("set-engine-order", ids),
|
||||||
|
removeFromList: (id) => ipcRenderer.invoke("remove-from-list", id),
|
||||||
|
// Main asks settings to jump to a specific sidebar section (e.g. from the
|
||||||
|
// engine picker's "Search settings…" click). Emits the section id string.
|
||||||
|
onFocusSection: (cb) => ipcRenderer.on("focus-section", (_e, section) => cb(section)),
|
||||||
// Collision-mode: BCNR/ICANN policy + per-name/per-TLD overrides
|
// Collision-mode: BCNR/ICANN policy + per-name/per-TLD overrides
|
||||||
collisionState: () => ipcRenderer.invoke("collision-state"),
|
collisionState: () => ipcRenderer.invoke("collision-state"),
|
||||||
setCollisionPolicy: (p) => ipcRenderer.invoke("collision-set-policy", p),
|
setCollisionPolicy: (p) => ipcRenderer.invoke("collision-set-policy", p),
|
||||||
|
|
|
||||||
|
|
@ -89,8 +89,21 @@
|
||||||
.eng .grip:active{cursor:grabbing}
|
.eng .grip:active{cursor:grabbing}
|
||||||
.eng.dragging{opacity:.45}
|
.eng.dragging{opacity:.45}
|
||||||
.eng.over{border-color:var(--acid);box-shadow:0 -2px 0 var(--acid) inset}
|
.eng.over{border-color:var(--acid);box-shadow:0 -2px 0 var(--acid) inset}
|
||||||
|
.eng.off{opacity:.55}
|
||||||
|
.eng.off .enm{color:var(--mut)}
|
||||||
.ehdr{font-size:11px;letter-spacing:.12em;text-transform:uppercase;color:var(--dim);margin:14px 0 6px;padding-top:2px}
|
.ehdr{font-size:11px;letter-spacing:.12em;text-transform:uppercase;color:var(--dim);margin:14px 0 6px;padding-top:2px}
|
||||||
.ehdr:first-child{margin-top:0}
|
.ehdr:first-child{margin-top:0}
|
||||||
|
/* right-click context menu for an engine row */
|
||||||
|
.ctxmenu{position:fixed;z-index:9999;background:#1c222c;border:1px solid var(--line);border-radius:8px;
|
||||||
|
box-shadow:0 12px 34px #000c;padding:4px;min-width:180px;font-size:13px;color:var(--ink)}
|
||||||
|
.ctxmenu .mi{padding:7px 12px;border-radius:5px;cursor:pointer;white-space:nowrap}
|
||||||
|
.ctxmenu .mi:hover{background:#ffffff10}
|
||||||
|
.ctxmenu .mi.danger{color:#f6768a}
|
||||||
|
.ctxmenu .mi.danger:hover{background:rgba(246,118,138,.12)}
|
||||||
|
@media (prefers-color-scheme: light){
|
||||||
|
.ctxmenu{background:#ffffff;border-color:rgba(0,0,0,.15)}
|
||||||
|
.ctxmenu .mi:hover{background:rgba(0,0,0,.05)}
|
||||||
|
}
|
||||||
/* engine catalog panel — appears under the enabled list when "+ Add" is clicked */
|
/* engine catalog panel — appears under the enabled list when "+ Add" is clicked */
|
||||||
.engcat{margin-top:6px;padding:12px 12px 10px;background:#0e131c;border:1px solid var(--line);border-radius:10px}
|
.engcat{margin-top:6px;padding:12px 12px 10px;background:#0e131c;border:1px solid var(--line);border-radius:10px}
|
||||||
.engcat .cat{display:flex;align-items:center;gap:10px;padding:6px 8px;border-radius:6px;font-size:13px}
|
.engcat .cat{display:flex;align-items:center;gap:10px;padding:6px 8px;border-radius:6px;font-size:13px}
|
||||||
|
|
@ -326,10 +339,15 @@
|
||||||
const C = window.cfg;
|
const C = window.cfg;
|
||||||
// sidebar navigation
|
// sidebar navigation
|
||||||
const sections = ["general", "search", "naming", "performance", "privacy"];
|
const sections = ["general", "search", "naming", "performance", "privacy"];
|
||||||
document.querySelectorAll(".side a").forEach((a) => a.onclick = () => {
|
function showSection(sec) {
|
||||||
document.querySelectorAll(".side a").forEach((x) => x.classList.toggle("active", x === a));
|
if (!sections.includes(sec)) return;
|
||||||
for (const s of sections) document.getElementById(s).hidden = (s !== a.dataset.sec);
|
document.querySelectorAll(".side a").forEach((x) => x.classList.toggle("active", x.dataset.sec === sec));
|
||||||
});
|
for (const s of sections) document.getElementById(s).hidden = (s !== sec);
|
||||||
|
}
|
||||||
|
document.querySelectorAll(".side a").forEach((a) => a.onclick = () => showSection(a.dataset.sec));
|
||||||
|
// Main-process asks us to jump to a section (e.g. picker's "Search settings…"
|
||||||
|
// click routes to the Search section instead of the General default).
|
||||||
|
if (C && C.onFocusSection) C.onFocusSection((sec) => showSection(sec));
|
||||||
|
|
||||||
const TOGGLES = ["restoreSession", "backgroundThrottle", "blockCamera", "blockMicrophone", "hideMediaDevices"];
|
const TOGGLES = ["restoreSession", "backgroundThrottle", "blockCamera", "blockMicrophone", "hideMediaDevices"];
|
||||||
C.get().then((s) => {
|
C.get().then((s) => {
|
||||||
|
|
@ -358,24 +376,25 @@
|
||||||
return opts ? `<optgroup label="${label}">${opts}</optgroup>` : "";
|
return opts ? `<optgroup label="${label}">${opts}</optgroup>` : "";
|
||||||
}).join("");
|
}).join("");
|
||||||
sel.value = d.current;
|
sel.value = d.current;
|
||||||
// Main list = the engines the user has enabled (i.e. actually shown in the
|
// Main list = engines the user has INSTALLED. The toggle only flips
|
||||||
// toolbar dropdown). Everything else lives in the catalog panel below and
|
// enabled/disabled — the row STAYS. Right-click a row → "Remove from
|
||||||
// is added via "+ Add". Drag reorders within a kind; toggling off moves an
|
// list" is what actually removes an engine (back to the catalog for
|
||||||
// engine back to the catalog; custom engines get a ✕ to delete entirely.
|
// built-ins, permanently for customs).
|
||||||
const list = document.getElementById("engineList");
|
const list = document.getElementById("engineList");
|
||||||
const rowFor = (e) => `<div class="eng" data-id="${e.id}" data-kind="${e.kind || "search"}" draggable="true">` +
|
const installed = d.engines.filter((e) => e.installed);
|
||||||
|
const rowFor = (e) => `<div class="eng${e.enabled ? "" : " off"}" data-id="${e.id}" data-kind="${e.kind || "search"}" data-builtin="${e.builtin ? 1 : 0}" draggable="true">` +
|
||||||
`<span class="grip" title="Drag to reorder">⠿</span>` +
|
`<span class="grip" title="Drag to reorder">⠿</span>` +
|
||||||
`<span class="eic">${engIcon(e)}</span><span class="enm">${esc(e.name)}</span>` +
|
`<span class="eic">${engIcon(e)}</span><span class="enm">${esc(e.name)}</span>` +
|
||||||
(e.builtin
|
`<label class="sw sm" title="${e.enabled ? "Turn off" : "Turn on"}"><input type="checkbox" data-id="${e.id}" ${e.enabled ? "checked" : ""}><span class="track"><span class="knob"></span></span></label>` +
|
||||||
? `<label class="sw sm"><input type="checkbox" data-id="${e.id}" ${e.enabled ? "checked" : ""}><span class="track"><span class="knob"></span></span></label>`
|
`</div>`;
|
||||||
: `<button class="cx" data-id="${e.id}" title="Remove">✕</button>`) + `</div>`;
|
|
||||||
list.innerHTML = ENGINE_KINDS.map(({ key, label }) => {
|
list.innerHTML = ENGINE_KINDS.map(({ key, label }) => {
|
||||||
const rows = enabled.filter((e) => (e.kind || "search") === key).map(rowFor).join("");
|
const rows = installed.filter((e) => (e.kind || "search") === key).map(rowFor).join("");
|
||||||
if (!rows) return "";
|
if (!rows) return "";
|
||||||
return `<div class="ehdr">${label}</div>${rows}`;
|
return `<div class="ehdr">${label}</div>${rows}`;
|
||||||
}).join("");
|
}).join("");
|
||||||
// Two catalog panes, split by tier:
|
// Two catalog panes, split by tier — filtered by !installed now, not
|
||||||
// catalog — curated first-class built-ins the user hasn't enabled
|
// !enabled (a toggled-off engine stays in the enabled list, not here):
|
||||||
|
// catalog — curated first-class built-ins the user hasn't installed
|
||||||
// extra — wider bank, filtered live by the "Discover more" search box
|
// extra — wider bank, filtered live by the "Discover more" search box
|
||||||
const cat = document.getElementById("catalogList");
|
const cat = document.getElementById("catalogList");
|
||||||
const extra = document.getElementById("extraList");
|
const extra = document.getElementById("extraList");
|
||||||
|
|
@ -385,13 +404,13 @@
|
||||||
`<span class="enm">${esc(e.name)}</span>` +
|
`<span class="enm">${esc(e.name)}</span>` +
|
||||||
`<span class="kind">${(e.kind || "search") === "llm" ? "AI" : "Search"}</span>` +
|
`<span class="kind">${(e.kind || "search") === "llm" ? "AI" : "Search"}</span>` +
|
||||||
`<button class="add" data-add="${e.id}">+ Add</button></div>`;
|
`<button class="add" data-add="${e.id}">+ Add</button></div>`;
|
||||||
const off = d.engines.filter((e) => e.builtin && !e.enabled);
|
const uninstalled = d.engines.filter((e) => e.builtin && !e.installed);
|
||||||
const catalogOff = off.filter((e) => (e.tier || "catalog") === "catalog");
|
const catalogOff = uninstalled.filter((e) => (e.tier || "catalog") === "catalog");
|
||||||
const extraOff = off.filter((e) => e.tier === "extra");
|
const extraOff = uninstalled.filter((e) => e.tier === "extra");
|
||||||
if (cat) {
|
if (cat) {
|
||||||
cat.innerHTML = catalogOff.length
|
cat.innerHTML = catalogOff.length
|
||||||
? catalogOff.map(catRow).join("")
|
? catalogOff.map(catRow).join("")
|
||||||
: `<div class="cempty2">All curated engines are enabled. Discover more below or add a custom URL.</div>`;
|
: `<div class="cempty2">All curated engines are already in your list. Discover more below or add a custom URL.</div>`;
|
||||||
cat.querySelectorAll(".add").forEach((b) => b.onclick = () => C.setEngineEnabled(b.dataset.add, true).then(renderEngines));
|
cat.querySelectorAll(".add").forEach((b) => b.onclick = () => C.setEngineEnabled(b.dataset.add, true).then(renderEngines));
|
||||||
}
|
}
|
||||||
if (extra) {
|
if (extra) {
|
||||||
|
|
@ -400,7 +419,7 @@
|
||||||
const shown = filt ? extraOff.filter((e) => e.name.toLowerCase().includes(filt)) : extraOff;
|
const shown = filt ? extraOff.filter((e) => e.name.toLowerCase().includes(filt)) : extraOff;
|
||||||
extra.innerHTML = shown.length
|
extra.innerHTML = shown.length
|
||||||
? shown.map(catRow).join("")
|
? shown.map(catRow).join("")
|
||||||
: `<div class="cempty2">${filt ? "No engines match that filter." : "All discoverable engines are enabled."}</div>`;
|
: `<div class="cempty2">${filt ? "No engines match that filter." : "All discoverable engines are already in your list."}</div>`;
|
||||||
extra.querySelectorAll(".add").forEach((b) => b.onclick = () => C.setEngineEnabled(b.dataset.add, true).then(renderEngines));
|
extra.querySelectorAll(".add").forEach((b) => b.onclick = () => C.setEngineEnabled(b.dataset.add, true).then(renderEngines));
|
||||||
};
|
};
|
||||||
paintExtras(filterInput ? filterInput.value : "");
|
paintExtras(filterInput ? filterInput.value : "");
|
||||||
|
|
@ -409,8 +428,16 @@
|
||||||
filterInput.addEventListener("input", () => paintExtras(filterInput.value));
|
filterInput.addEventListener("input", () => paintExtras(filterInput.value));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Toggle: pure on/off in the enabled set — the row stays visible either way.
|
||||||
list.querySelectorAll('input[type="checkbox"]').forEach((cb) => cb.onchange = () => C.setEngineEnabled(cb.dataset.id, cb.checked).then(renderEngines));
|
list.querySelectorAll('input[type="checkbox"]').forEach((cb) => cb.onchange = () => C.setEngineEnabled(cb.dataset.id, cb.checked).then(renderEngines));
|
||||||
list.querySelectorAll(".cx").forEach((b) => b.onclick = () => C.removeEngine(b.dataset.id).then(renderEngines));
|
// Right-click any row → context menu with "Remove from list" (moves a
|
||||||
|
// built-in back to the catalog; deletes a custom entirely).
|
||||||
|
list.querySelectorAll(".eng").forEach((row) => {
|
||||||
|
row.addEventListener("contextmenu", (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
openEngineMenu(row, e.clientX, e.clientY);
|
||||||
|
});
|
||||||
|
});
|
||||||
// drag-and-drop reorder — same-kind only (dropping a Search engine into
|
// drag-and-drop reorder — same-kind only (dropping a Search engine into
|
||||||
// the LLM section would just re-group visually on next render, so we
|
// the LLM section would just re-group visually on next render, so we
|
||||||
// reject cross-kind drags outright).
|
// reject cross-kind drags outright).
|
||||||
|
|
@ -436,6 +463,35 @@
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
// Floating right-click menu for an engine row. Only one open at a time.
|
||||||
|
let ctxOpen = null;
|
||||||
|
function closeEngineMenu() { if (ctxOpen) { ctxOpen.remove(); ctxOpen = null; } }
|
||||||
|
function openEngineMenu(row, x, y) {
|
||||||
|
closeEngineMenu();
|
||||||
|
const id = row.dataset.id;
|
||||||
|
const builtin = row.dataset.builtin === "1";
|
||||||
|
const m = document.createElement("div");
|
||||||
|
m.className = "ctxmenu";
|
||||||
|
m.innerHTML = `<div class="mi danger" data-act="remove">Remove from list</div>`;
|
||||||
|
document.body.appendChild(m);
|
||||||
|
// Position, keeping the menu inside the viewport.
|
||||||
|
const rect = m.getBoundingClientRect();
|
||||||
|
const vw = document.documentElement.clientWidth, vh = document.documentElement.clientHeight;
|
||||||
|
m.style.left = Math.min(x, vw - rect.width - 6) + "px";
|
||||||
|
m.style.top = Math.min(y, vh - rect.height - 6) + "px";
|
||||||
|
m.querySelector('[data-act="remove"]').onclick = () => {
|
||||||
|
closeEngineMenu();
|
||||||
|
const call = builtin ? C.removeFromList(id) : C.removeEngine(id);
|
||||||
|
call.then(renderEngines);
|
||||||
|
};
|
||||||
|
ctxOpen = m;
|
||||||
|
setTimeout(() => {
|
||||||
|
const off = (ev) => { if (!m.contains(ev.target)) { closeEngineMenu(); document.removeEventListener("mousedown", off); document.removeEventListener("keydown", esc); } };
|
||||||
|
const esc = (ev) => { if (ev.key === "Escape") { closeEngineMenu(); document.removeEventListener("mousedown", off); document.removeEventListener("keydown", esc); } };
|
||||||
|
document.addEventListener("mousedown", off);
|
||||||
|
document.addEventListener("keydown", esc);
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
sel.onchange = () => C.set("searchEngine", sel.value);
|
sel.onchange = () => C.set("searchEngine", sel.value);
|
||||||
// appearance (theme) — three visual cards: system | light | dark. Any
|
// appearance (theme) — three visual cards: system | light | dark. Any
|
||||||
// unrecognised saved value falls back to "system" (follow the OS).
|
// unrecognised saved value falls back to "system" (follow the OS).
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue