Theseus: per-engine symbols + light-theme cascade fix

- Each search engine shows a symbol in the toolbar dropdown, the placeholder,
  and Settings (🦆 DuckDuckGo, 🦁 Brave, 🔵 Google, 🔎 Bing, 🛡️ Startpage,
  🔴 Yandex); custom engines take an optional symbol (defaults to 🔍).
- Fix: light-theme @media blocks for settings/popover/home were placed before
  the base rules and lost the cascade (sidebar stayed dark) — moved them last.
This commit is contained in:
Local Dev 2026-07-30 22:09:26 +02:00
parent 2757cd8c64
commit ad34ad3bbe
3 changed files with 47 additions and 21 deletions

View file

@ -158,11 +158,11 @@
function fillEngines(d) { function fillEngines(d) {
engineCurrent = d.current; engineCurrent = d.current;
const sel = $("engine"); const sel = $("engine");
sel.innerHTML = d.engines.map((e) => `<option value="${e.id}">${e.name.replace(/</g,"&lt;")}</option>`).join("") sel.innerHTML = d.engines.map((e) => `<option value="${e.id}">${(e.sym ? e.sym + " " : "")}${e.name.replace(/</g,"&lt;")}</option>`).join("")
+ `<option disabled>──────────</option><option value="__manage"> Add / edit engines…</option>`; + `<option disabled>──────────</option><option value="__manage"> Add / edit engines…</option>`;
sel.value = d.current; sel.value = d.current;
const cur = d.engines.find((e) => e.id === d.current); const cur = d.engines.find((e) => e.id === d.current);
$("search").placeholder = cur ? "Search with " + cur.name : "Search"; $("search").placeholder = cur ? "Search with " + (cur.sym ? cur.sym + " " : "") + cur.name : "Search";
} }
$("engine").onchange = () => { $("engine").onchange = () => {
const v = $("engine").value; const v = $("engine").value;

52
main.js
View file

@ -24,17 +24,17 @@ const RESOLVER = app.isPackaged
// Built-in search engines. Users can also add their own (settings.customEngines, // Built-in search engines. Users can also add their own (settings.customEngines,
// each { id, name, url } where the url contains "%s" for the query). // each { id, name, url } where the url contains "%s" for the query).
const SEARCH_ENGINES = { const SEARCH_ENGINES = {
duckduckgo: { name: "DuckDuckGo", url: (q) => "https://duckduckgo.com/?q=" + encodeURIComponent(q) }, duckduckgo: { name: "DuckDuckGo", sym: "🦆", url: (q) => "https://duckduckgo.com/?q=" + encodeURIComponent(q) },
google: { name: "Google", url: (q) => "https://www.google.com/search?q=" + encodeURIComponent(q) }, google: { name: "Google", sym: "🔵", url: (q) => "https://www.google.com/search?q=" + encodeURIComponent(q) },
brave: { name: "Brave", url: (q) => "https://search.brave.com/search?q=" + encodeURIComponent(q) }, brave: { name: "Brave", sym: "🦁", url: (q) => "https://search.brave.com/search?q=" + encodeURIComponent(q) },
bing: { name: "Bing", url: (q) => "https://www.bing.com/search?q=" + encodeURIComponent(q) }, bing: { name: "Bing", sym: "🔎", url: (q) => "https://www.bing.com/search?q=" + encodeURIComponent(q) },
startpage: { name: "Startpage", url: (q) => "https://www.startpage.com/sp/search?query=" + encodeURIComponent(q) }, startpage: { name: "Startpage", sym: "🛡️", url: (q) => "https://www.startpage.com/sp/search?query=" + encodeURIComponent(q) },
yandex: { name: "Yandex", url: (q) => "https://yandex.com/search/?text=" + encodeURIComponent(q) }, yandex: { name: "Yandex", sym: "🔴", url: (q) => "https://yandex.com/search/?text=" + encodeURIComponent(q) },
}; };
// Built-in + user-added, as flat metadata for the pickers. // Built-in + user-added, as flat metadata for the pickers (sym = a display symbol).
function allEngines() { function allEngines() {
const list = Object.entries(SEARCH_ENGINES).map(([id, e]) => ({ id, name: e.name, builtin: true })); const list = Object.entries(SEARCH_ENGINES).map(([id, e]) => ({ id, name: e.name, sym: e.sym, builtin: true }));
for (const c of settings.customEngines || []) list.push({ id: c.id, name: c.name, builtin: false }); for (const c of settings.customEngines || []) list.push({ id: c.id, name: c.name, sym: c.sym || "🔍", builtin: false });
return list; return list;
} }
function engineUrl(id, q) { function engineUrl(id, q) {
@ -49,11 +49,15 @@ const GATEWAY = "https://navigate.st";
// ---- BNS name detection (multi-TLD) -------------------------------------- // ---- BNS name detection (multi-TLD) --------------------------------------
// The engine (resolver-web.js) resolves any <label>.<tld> from the BCNR beacon. // The engine (resolver-web.js) resolves any <label>.<tld> from the BCNR beacon.
// Two client-side sets decide how Theseus treats a name: // Two client-side sets decide how Theseus treats a name:
// NATIVE — not in the ICANN root, so we own them outright (go straight to BCNR). // NATIVE — BCNR is tried FIRST. If the TLD is also in DUAL (real ICANN),
// DUAL — real ICANN TLDs we ALSO offer on BCNR. These never hijack the real // a BCNR NXDOMAIN or resolver failure falls through to the real
// web: the ICANN site loads normally, and if a BCNR name also exists // web instead of erroring. If the TLD is NOT in DUAL, BCNR is
// we surface a passive "also on BCNR" switch (see navigateTab). // authoritative and NXDOMAIN shows a clean bns:// error page.
const BNS_NATIVE_TLDS = new Set(["bch", "p2p", "bit", "nav", "x", "sia", "dgb", "nt"]); // DUAL — Real ICANN TLDs. Used two ways:
// (a) present in NATIVE too — unlocks the ICANN fallback above;
// (b) present in DUAL only — Theseus loads the ICANN site
// normally and surfaces a passive "also on BCNR" switch.
const BNS_NATIVE_TLDS = new Set(["bch", "p2p", "bit", "nav", "x", "sia", "dgb", "nt", "de", "ru"]);
const BNS_DUAL_TLDS = new Set(["de", "dev", "ltd", "ru"]); const BNS_DUAL_TLDS = new Set(["de", "dev", "ltd", "ru"]);
const REGISTRY = "BCNR"; // user-facing registry label (Bitcoin Cash Name Registry) const REGISTRY = "BCNR"; // user-facing registry label (Bitcoin Cash Name Registry)
const tldOf = (host) => { const tldOf = (host) => {
@ -633,11 +637,27 @@ async function navigateTab(id, input) {
async function loadBns(t, id, host, rest, tld) { async function loadBns(t, id, host, rest, tld) {
const registry = registryOf(tld); const registry = registryOf(tld);
if (id === activeId) pushNav({ host, kind: "resolving", tld, registry }); if (id === activeId) pushNav({ host, kind: "resolving", tld, registry });
// A NATIVE TLD that is ALSO in DUAL is a real ICANN TLD — BCNR is priority,
// but a miss (NXDOMAIN or resolver failure) falls through to the real web so
// users aren't locked out of e.g. bank.de because a name isn't on chain or
// the beacon is unreachable.
const canFallbackToWeb = BNS_DUAL_TLDS.has(tld);
const fallbackToWeb = async (reason) => {
t.url = "https://" + host + (rest === "/" ? "" : rest);
try { await t.view.webContents.loadURL(t.url); } catch {}
t.prov = { host, kind: "web", note: `fallback: ${reason}`, tld, registry };
if (id === activeId) pushNav(t.prov);
emitTabs();
};
let entry; let entry;
try { entry = await resolveHost(host); } try { entry = await resolveHost(host); }
catch (e) { setLoading(t, false); t.prov = { host, kind: "error", error: e.message, tld, registry }; if (id === activeId) pushNav(t.prov); return; } catch (e) {
if (canFallbackToWeb) { setLoading(t, false); return fallbackToWeb("BCNR unreachable"); }
setLoading(t, false); t.prov = { host, kind: "error", error: e.message, tld, registry }; if (id === activeId) pushNav(t.prov); return;
}
t.url = host + (rest === "/" ? "" : rest); t.url = host + (rest === "/" ? "" : rest);
if (!entry) { if (!entry) {
if (canFallbackToWeb) { setLoading(t, false); return fallbackToWeb("no BCNR record"); }
t.prov = { host, kind: "nxdomain", tld, registry }; t.prov = { host, kind: "nxdomain", tld, registry };
await t.view.webContents.loadURL(`bns://${host}/`); await t.view.webContents.loadURL(`bns://${host}/`);
if (id === activeId) pushNav(t.prov); emitTabs(); return; if (id === activeId) pushNav(t.prov); emitTabs(); return;
@ -678,7 +698,7 @@ ipcMain.handle("add-engine", (_e, eng) => {
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), 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) }];
settings.searchEngine = id; // select the one just added settings.searchEngine = id; // select the one just added
saveSettings(); emitEngines(); saveSettings(); emitEngines();
} }

View file

@ -34,6 +34,7 @@
.btn{border:1px solid #d6ff3d55;background:rgba(214,255,61,.12);color:#eaffb0;border-radius:8px;padding:8px 14px;font-size:13px;cursor:pointer} .btn{border:1px solid #d6ff3d55;background:rgba(214,255,61,.12);color:#eaffb0;border-radius:8px;padding:8px 14px;font-size:13px;cursor:pointer}
.btn:hover{background:rgba(214,255,61,.22)} .btn:hover{background:rgba(214,255,61,.22)}
.ceng{display:flex;align-items:center;gap:8px;background:#10151f;border:1px solid var(--line);border-radius:8px;padding:6px 10px;margin-bottom:6px;font-size:13px} .ceng{display:flex;align-items:center;gap:8px;background:#10151f;border:1px solid var(--line);border-radius:8px;padding:6px 10px;margin-bottom:6px;font-size:13px}
.ceng .cs{font-size:14px;flex:none}
.ceng .cn{font-weight:600} .ceng .cu{color:var(--dim);font-size:12px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1} .ceng .cn{font-weight:600} .ceng .cu{color:var(--dim);font-size:12px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1}
.ceng .cx{cursor:pointer;color:#8b98a9;border:none;background:transparent;font-size:13px} .ceng .cx:hover{color:#f6768a} .ceng .cx{cursor:pointer;color:#8b98a9;border:none;background:transparent;font-size:13px} .ceng .cx:hover{color:#f6768a}
.cempty{color:var(--dim);font-size:12.5px} .cempty{color:var(--dim);font-size:12.5px}
@ -83,6 +84,7 @@
<div class="d">Name it, and give a URL with <code>%s</code> where the query goes — e.g. <code>https://searx.be/search?q=%s</code></div></div> <div class="d">Name it, and give a URL with <code>%s</code> where the query goes — e.g. <code>https://searx.be/search?q=%s</code></div></div>
<div id="customList"></div> <div id="customList"></div>
<div class="addeng"> <div class="addeng">
<input id="engSym" placeholder="🔍" style="max-width:52px;text-align:center;flex:none">
<input id="engName" placeholder="Name (e.g. SearXNG)"> <input id="engName" placeholder="Name (e.g. SearXNG)">
<input id="engUrl" placeholder="https://example.com/search?q=%s"> <input id="engUrl" placeholder="https://example.com/search?q=%s">
<button id="engAdd" class="btn">Add</button> <button id="engAdd" class="btn">Add</button>
@ -176,12 +178,12 @@
const sel = document.getElementById("searchEngine"); const sel = document.getElementById("searchEngine");
const esc = (s) => String(s || "").replace(/</g, "&lt;"); const esc = (s) => String(s || "").replace(/</g, "&lt;");
function renderEngines(d) { function renderEngines(d) {
sel.innerHTML = d.engines.map((e) => `<option value="${e.id}">${esc(e.name)}</option>`).join(""); sel.innerHTML = d.engines.map((e) => `<option value="${e.id}">${(e.sym ? e.sym + " " : "")}${esc(e.name)}</option>`).join("");
sel.value = d.current; sel.value = d.current;
const custom = d.engines.filter((e) => !e.builtin); const custom = d.engines.filter((e) => !e.builtin);
const list = document.getElementById("customList"); const list = document.getElementById("customList");
list.innerHTML = custom.length list.innerHTML = custom.length
? custom.map((e) => `<div class="ceng"><span class="cn">${esc(e.name)}</span><span class="cu">${esc(e.id)}</span><button class="cx" data-id="${e.id}" title="Remove"></button></div>`).join("") ? custom.map((e) => `<div class="ceng"><span class="cs">${e.sym || "🔍"}</span><span class="cn">${esc(e.name)}</span><span class="cu">${esc(e.id)}</span><button class="cx" data-id="${e.id}" title="Remove"></button></div>`).join("")
: `<div class="cempty">No custom engines yet.</div>`; : `<div class="cempty">No custom engines yet.</div>`;
list.querySelectorAll(".cx").forEach((b) => b.onclick = () => C.removeEngine(b.dataset.id).then(renderEngines)); list.querySelectorAll(".cx").forEach((b) => b.onclick = () => C.removeEngine(b.dataset.id).then(renderEngines));
} }
@ -197,8 +199,12 @@
document.getElementById("engAdd").onclick = () => { document.getElementById("engAdd").onclick = () => {
const name = document.getElementById("engName").value.trim(); const name = document.getElementById("engName").value.trim();
const url = document.getElementById("engUrl").value.trim(); const url = document.getElementById("engUrl").value.trim();
const sym = document.getElementById("engSym").value.trim();
if (!name || !url.includes("%s")) { alert("Enter a name and a URL containing %s (where the query goes)."); return; } if (!name || !url.includes("%s")) { alert("Enter a name and a URL containing %s (where the query goes)."); return; }
C.addEngine({ name, url }).then((d) => { document.getElementById("engName").value = ""; document.getElementById("engUrl").value = ""; renderEngines(d); }); C.addEngine({ name, url, sym }).then((d) => {
document.getElementById("engName").value = ""; document.getElementById("engUrl").value = ""; document.getElementById("engSym").value = "";
renderEngines(d);
});
}; };
C.engines().then(renderEngines); C.engines().then(renderEngines);
// anti-fingerprinting mode selectors, with value field(s) shown on "manual" // anti-fingerprinting mode selectors, with value field(s) shown on "manual"