diff --git a/chrome.html b/chrome.html index 12be37e..82bd83c 100644 --- a/chrome.html +++ b/chrome.html @@ -67,7 +67,7 @@ background: transparent; cursor: pointer; padding: 0; color: var(--dim); } .secbadge:hover { background: var(--line2); } .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.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; } diff --git a/main.js b/main.js index 16418cb..3bf446b 100644 --- a/main.js +++ b/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); 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); } -// Built-in + user-added, as flat metadata for the pickers. Built-in obey -// enabledEngines; custom engines are always enabled. +// Two-tier state: an engine is INSTALLED if it's in the user's Additional +// 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() { 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 || []) - 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). const order = settings.engineOrder || []; 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 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) - 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) customEngines: [], // user-added: [{ id, name, url-with-%s }] 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(); } } +// 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) { const t = tabById(id); if (!t) return; t.url = ""; t.title = "Theseus"; t.prov = { host: "", kind: "home" }; @@ -779,8 +799,8 @@ function createTab(initial, opts = {}) { tabs.push(tab); win.contentView.addChildView(view); wc.on("page-title-updated", (_e, title) => { tab.title = title; emitTabs(); }); - wc.on("did-navigate", () => emitTabs()); - wc.on("did-navigate-in-page", () => emitTabs()); + wc.on("did-navigate", () => { refreshTabUrl(tab); emitTabs(); }); + wc.on("did-navigate-in-page", () => { refreshTabUrl(tab); emitTabs(); }); wc.on("did-start-loading", () => setLoading(tab, true)); wc.on("did-stop-loading", () => setLoading(tab, false)); wc.on("will-navigate", (e, u) => { @@ -1092,28 +1112,58 @@ ipcMain.handle("set-search-engine", (_e, id) => { return settings.searchEngine; }); 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")) { const id = "custom-" + Date.now().toString(36); 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) }]; + // Custom engines are auto-installed and enabled. + settings.enabledEngines = [...new Set([...(settings.enabledEngines || DEFAULT_ENABLED), id])]; settings.searchEngine = id; // select the one just added saveSettings(); emitEngines(); } return { engines: allEngines(), current: settings.searchEngine }; }); 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.enabledEngines = (settings.enabledEngines || DEFAULT_ENABLED).filter((x) => x !== id); if (settings.searchEngine === id) settings.searchEngine = enabledEnginesList()[0]?.id || "duckduckgo"; saveSettings(); emitEngines(); 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) => { if (SEARCH_ENGINES[id]) { - let list = (settings.enabledEngines || DEFAULT_ENABLED).filter((x) => x !== id); - if (on) list.push(id); - settings.enabledEngines = list.length ? list : ["duckduckgo"]; // never empty + let installed = (settings.installedEngines || DEFAULT_ENABLED).slice(); + let enabled = (settings.enabledEngines || DEFAULT_ENABLED).filter((x) => x !== id); + 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)) settings.searchEngine = enabledEnginesList()[0]?.id || "duckduckgo"; saveSettings(); emitEngines(); @@ -1136,7 +1186,15 @@ ipcMain.handle("pick-engine", (_e, id) => { if (enabledEnginesList().some((e) => e.id === id)) { settings.searchEngine = id; saveSettings(); emitEngines(); } 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 -------------------------------------------------------------- ipcMain.handle("downloads-get", () => downloadsPublic()); ipcMain.handle("toggle-downloads", (_e, rect) => { diff --git a/settings-preload.js b/settings-preload.js index 3f361db..d6dd025 100644 --- a/settings-preload.js +++ b/settings-preload.js @@ -7,6 +7,10 @@ contextBridge.exposeInMainWorld("cfg", { removeEngine: (id) => ipcRenderer.invoke("remove-engine", id), setEngineEnabled: (id, on) => ipcRenderer.invoke("set-engine-enabled", id, on), 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 collisionState: () => ipcRenderer.invoke("collision-state"), setCollisionPolicy: (p) => ipcRenderer.invoke("collision-set-policy", p), diff --git a/settings.html b/settings.html index 0509884..7340a37 100644 --- a/settings.html +++ b/settings.html @@ -89,8 +89,21 @@ .eng .grip:active{cursor:grabbing} .eng.dragging{opacity:.45} .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: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 */ .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} @@ -326,10 +339,15 @@ const C = window.cfg; // sidebar navigation const sections = ["general", "search", "naming", "performance", "privacy"]; - document.querySelectorAll(".side a").forEach((a) => a.onclick = () => { - document.querySelectorAll(".side a").forEach((x) => x.classList.toggle("active", x === a)); - for (const s of sections) document.getElementById(s).hidden = (s !== a.dataset.sec); - }); + function showSection(sec) { + if (!sections.includes(sec)) return; + 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"]; C.get().then((s) => { @@ -358,24 +376,25 @@ return opts ? `${opts}` : ""; }).join(""); sel.value = d.current; - // Main list = the engines the user has enabled (i.e. actually shown in the - // toolbar dropdown). Everything else lives in the catalog panel below and - // is added via "+ Add". Drag reorders within a kind; toggling off moves an - // engine back to the catalog; custom engines get a βœ• to delete entirely. + // Main list = engines the user has INSTALLED. The toggle only flips + // enabled/disabled β€” the row STAYS. Right-click a row β†’ "Remove from + // list" is what actually removes an engine (back to the catalog for + // built-ins, permanently for customs). const list = document.getElementById("engineList"); - const rowFor = (e) => `
` + + const installed = d.engines.filter((e) => e.installed); + const rowFor = (e) => `
` + `β Ώ` + `${engIcon(e)}${esc(e.name)}` + - (e.builtin - ? `` - : ``) + `
`; + `` + + `
`; 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 ""; return `
${label}
${rows}`; }).join(""); - // Two catalog panes, split by tier: - // catalog β€” curated first-class built-ins the user hasn't enabled + // Two catalog panes, split by tier β€” filtered by !installed now, not + // !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 const cat = document.getElementById("catalogList"); const extra = document.getElementById("extraList"); @@ -385,13 +404,13 @@ `${esc(e.name)}` + `${(e.kind || "search") === "llm" ? "AI" : "Search"}` + ``; - const off = d.engines.filter((e) => e.builtin && !e.enabled); - const catalogOff = off.filter((e) => (e.tier || "catalog") === "catalog"); - const extraOff = off.filter((e) => e.tier === "extra"); + const uninstalled = d.engines.filter((e) => e.builtin && !e.installed); + const catalogOff = uninstalled.filter((e) => (e.tier || "catalog") === "catalog"); + const extraOff = uninstalled.filter((e) => e.tier === "extra"); if (cat) { cat.innerHTML = catalogOff.length ? catalogOff.map(catRow).join("") - : `
All curated engines are enabled. Discover more below or add a custom URL.
`; + : `
All curated engines are already in your list. Discover more below or add a custom URL.
`; cat.querySelectorAll(".add").forEach((b) => b.onclick = () => C.setEngineEnabled(b.dataset.add, true).then(renderEngines)); } if (extra) { @@ -400,7 +419,7 @@ const shown = filt ? extraOff.filter((e) => e.name.toLowerCase().includes(filt)) : extraOff; extra.innerHTML = shown.length ? shown.map(catRow).join("") - : `
${filt ? "No engines match that filter." : "All discoverable engines are enabled."}
`; + : `
${filt ? "No engines match that filter." : "All discoverable engines are already in your list."}
`; extra.querySelectorAll(".add").forEach((b) => b.onclick = () => C.setEngineEnabled(b.dataset.add, true).then(renderEngines)); }; paintExtras(filterInput ? filterInput.value : ""); @@ -409,8 +428,16 @@ 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(".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 // the LLM section would just re-group visually on next render, so we // 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 = `
Remove from list
`; + 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); // appearance (theme) β€” three visual cards: system | light | dark. Any // unrecognised saved value falls back to "system" (follow the OS).