Theseus: light/dark theme, redesigned search box, trimmed engines, WebRTC modes

- Light/dark/system theme (Settings > General) via nativeTheme.themeSource
  driving prefers-color-scheme across chrome, settings, popover, and home.
- Search box redesigned: a magnifier icon opens the engine dropdown, the rest
  is a wider typing field; styled to match the browser (was a raw <select>).
- Trimmed built-in engines to DuckDuckGo/Google/Brave/Bing/Startpage/Yandex;
  custom engines still add/remove via "Add / edit engines…".
- WebRTC: the "WebRTC Network Limiter" extension can't run in Electron
  (chrome.privacy API is unavailable), so its function is now a native
  4-mode WebRTC IP policy in Settings > Privacy.
- Address-bar placeholder: "Ask a search engine or enter web address".
This commit is contained in:
Local Dev 2026-07-30 22:00:01 +02:00
parent 01429990ea
commit 2757cd8c64
5 changed files with 133 additions and 52 deletions

View file

@ -2,28 +2,40 @@
<html>
<head><meta charset="utf-8">
<style>
:root { color-scheme: dark; font-family: system-ui, sans-serif; }
:root {
color-scheme: light dark; font-family: system-ui, sans-serif;
--bg:#0f1420; --surface:#1b2330; --surface2:#171e2a; --active:#263041cc;
--ink:#e7eaf1; --mut:#b9c2d0; --dim:#8b98a9; --faint:#5e6678;
--line:rgba(255,255,255,.20); --line2:rgba(255,255,255,.13); --hover:rgba(255,255,255,.08);
}
@media (prefers-color-scheme: light) {
:root {
--bg:#e9ecf1; --surface:#ffffff; --surface2:#f1f3f7; --active:#dbe1ec;
--ink:#1a1f28; --mut:#3c4453; --dim:#697280; --faint:#98a1b0;
--line:rgba(0,0,0,.18); --line2:rgba(0,0,0,.11); --hover:rgba(0,0,0,.06);
}
}
[hidden] { display: none !important; }
body { margin: 0; height: 100vh; background: #0f1420; color: #e7eaf1; user-select: none; overflow: hidden; }
body { margin: 0; height: 100vh; background: var(--bg); color: var(--ink); user-select: none; overflow: hidden; }
.tabs { display: flex; align-items: flex-end; gap: 4px; padding: 6px 8px 0; height: 34px; }
.tab { display: flex; align-items: center; gap: 8px; max-width: 200px; min-width: 90px; padding: 6px 10px;
background: #1b2330; border: 1px solid #ffffff14; border-bottom: none; border-radius: 8px 8px 0 0;
font-size: 12.5px; cursor: default; color: #b9c2d0; }
.tab.active { background: #26304180; color: #fff; }
background: var(--surface); border: 1px solid var(--line2); border-bottom: none; border-radius: 8px 8px 0 0;
font-size: 12.5px; cursor: default; color: var(--mut); }
.tab.active { background: var(--active); color: var(--ink); }
.tab .t { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex: 1; }
.tab .spin { width: 10px; height: 10px; flex: none; border: 1.5px solid #ffffff2e; border-top-color: #d6ff3d; border-radius: 50%; animation: spin .7s linear infinite; }
@keyframes spin { to { transform: rotate(360deg); } }
.tab .x { opacity: .5; cursor: pointer; padding: 0 2px; border-radius: 4px; }
.tab .x:hover { opacity: 1; background: #ffffff1a; }
.newtab { padding: 4px 10px; cursor: pointer; color: #8b98a9; border-radius: 6px; font-size: 16px; }
.newtab:hover { background: #ffffff12; color: #fff; }
.tab .x:hover { opacity: 1; background: var(--line2); }
.newtab { padding: 4px 10px; cursor: pointer; color: var(--dim); border-radius: 6px; font-size: 16px; }
.newtab:hover { background: var(--hover); color: var(--ink); }
.bar { display: flex; gap: 6px; align-items: center; padding: 6px 10px; }
.nav { display: flex; gap: 1px; }
.ic { width: 34px; height: 32px; display: grid; place-items: center; border-radius: 8px; cursor: pointer;
color: #c5cdda; background: transparent; border: none; padding: 0; }
color: var(--mut); background: transparent; border: none; padding: 0; }
.ic svg { width: 16px; height: 16px; fill: none; stroke: currentColor; stroke-width: 1.7; stroke-linecap: round; stroke-linejoin: round; }
.ic:hover { background: #ffffff14; color: #fff; }
.ic:active { background: #ffffff22; }
.ic:hover { background: var(--line2); color: var(--ink); }
.ic:active { background: var(--line); }
.ic:disabled { opacity: .28; cursor: default; background: transparent; }
/* indeterminate loading bar under the toolbar */
.loadbar { height: 2px; overflow: hidden; background: transparent; }
@ -31,31 +43,36 @@
background: linear-gradient(90deg, transparent, #4b7bec, #d6ff3d, transparent); animation: loadslide 1.1s infinite linear; }
@keyframes loadslide { 0% { transform: translateX(-110%); } 100% { transform: translateX(400%); } }
/* address bar */
.urlwrap { flex: 1; display: flex; align-items: center; gap: 4px; background: #1b2330; border: 1px solid #ffffff22;
.urlwrap { flex: 1; display: flex; align-items: center; gap: 4px; background: var(--surface); border: 1px solid var(--line);
border-radius: 999px; padding: 0 6px 0 4px; }
.urlwrap:focus-within { border-color: #4b7bec; }
/* Firefox-style padlock — always present (the "site security" button) */
.secbadge { display: grid; place-items: center; width: 28px; height: 26px; border-radius: 6px; border: none;
background: transparent; cursor: pointer; padding: 0; }
.secbadge:hover { background: #ffffff14; }
.secbadge:hover { background: var(--line2); }
.secbadge svg { width: 14px; height: 14px; }
.secbadge svg .shk { fill: none; stroke: #a9b3c2; stroke-width: 1.3; }
.secbadge svg .bdy { fill: #a9b3c2; }
.secbadge svg .shk { fill: none; stroke: var(--dim); stroke-width: 1.3; }
.secbadge svg .bdy { fill: var(--dim); }
.secbadge.warn svg .shk { stroke: #f6ad55; } .secbadge.warn svg .bdy { fill: #f6ad55; }
input { padding: 7px 6px; border-radius: 999px; border: none; background: transparent; color: inherit; font-size: 13.5px; outline: none; }
#url { flex: 1; }
/* minimal registry indicator at the END of the address bar */
.reg { font-size: 10.5px; letter-spacing: .03em; padding: 2px 9px; border-radius: 999px; white-space: nowrap;
background: rgba(214,255,61,.13); color: #d6ff3d; border: 1px solid #d6ff3d33; }
.reg.icann { background: rgba(139,152,169,.14); color: #9aa6b6; border-color: #ffffff1a; }
.star { border: none; background: transparent; cursor: pointer; color: #8b98a9; font-size: 15px; padding: 2px 4px; border-radius: 6px; }
.star:hover { background: #ffffff14; color: #fff; } .star.on { color: #ffd23d; }
/* compact search-engine picker */
.engine { display: flex; align-items: center; gap: 4px; background: #1b2330; border: 1px solid #ffffff22;
border-radius: 8px; padding: 0 4px 0 8px; }
.engine .mag { color: #8b98a9; font-size: 12px; }
.engine select { background: transparent; color: #c5cdda; border: none; font-size: 12px; padding: 7px 2px; outline: none; cursor: pointer; }
.tor { padding: 7px 10px; border-radius: 8px; border: 1px solid #ffffff22; background: #2b2f3a; color: #fff; cursor: pointer; white-space: nowrap; font-size: 12.5px; }
.reg.icann { background: rgba(139,152,169,.14); color: #9aa6b6; border-color: var(--line2); }
.star { border: none; background: transparent; cursor: pointer; color: var(--dim); font-size: 15px; padding: 2px 4px; border-radius: 6px; }
.star:hover { background: var(--line2); color: var(--ink); } .star.on { color: #ffd23d; }
/* search box: engine icon (click = pick engine) + a wide typing area */
.searchbox { display: flex; align-items: center; width: 300px; background: var(--surface); border: 1px solid var(--line);
border-radius: 999px; padding: 0 12px 0 3px; }
.searchbox:focus-within { border-color: #4b7bec; }
.engine-sel { appearance: none; -webkit-appearance: none; width: 34px; height: 30px; border: none; cursor: pointer;
color: transparent; border-radius: 999px 0 0 999px;
background: transparent url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%238b98a9' stroke-width='1.6'%3E%3Ccircle cx='7' cy='7' r='4.2'/%3E%3Cpath d='M10.2 10.2 L14 14'/%3E%3C/svg%3E") no-repeat center / 15px; }
.engine-sel:hover { background-color: var(--hover); }
.engine-sel option { color: var(--ink); background: var(--surface); }
#search { flex: 1; padding: 7px 4px; }
.tor { padding: 7px 10px; border-radius: 8px; border: 1px solid var(--line); background: #2b2f3a; color: var(--ink); cursor: pointer; white-space: nowrap; font-size: 12.5px; }
.tor.connecting { background: #7a5c14; } .tor.on { background: #6b3fa0; }
.logo { display: flex; align-items: center; gap: 6px; font-weight: 700; color: #d6ff3d; white-space: nowrap;
padding: 6px 12px; border: 1px solid #d6ff3d33; border-radius: 8px; cursor: pointer; background: rgba(214,255,61,.08); font-size: 13px; }
@ -64,23 +81,23 @@
/* favorites bar — new-tab page only, single compact row */
.bookmarks { display: flex; align-items: center; gap: 4px; padding: 3px 10px 5px; height: 26px; overflow: hidden; }
.bm { display: flex; align-items: center; gap: 6px; max-width: 180px; padding: 3px 9px; border-radius: 6px;
background: #171e2a; border: 1px solid #ffffff10; color: #c5cdda; font-size: 12px; cursor: pointer; white-space: nowrap; }
.bm:hover { background: #202a3a; color: #fff; }
background: var(--surface2); border: 1px solid var(--hover); color: var(--mut); font-size: 12px; cursor: pointer; white-space: nowrap; }
.bm:hover { background: var(--active); color: var(--ink); }
.bm .bt { overflow: hidden; text-overflow: ellipsis; }
.bm .bx { opacity: 0; cursor: pointer; font-size: 11px; } .bm:hover .bx { opacity: .55; }
.bm .bx:hover { opacity: 1; color: #f6768a; }
.bm-empty { color: #5e6678; font-size: 11.5px; white-space: nowrap; }
.bm-empty { color: var(--faint); font-size: 11.5px; white-space: nowrap; }
.tordisc { font-size: 11.5px; color: #d9c7f2; background: #2a1c40; border-top: 1px solid #6b3fa055; padding: 5px 14px; }
.tordisc a { color: #d6ff3d; }
.bcnrbar { display: flex; align-items: center; gap: 10px; font-size: 11.5px; color: #dbead0;
.bcnrbar { display: flex; align-items: center; gap: 10px; font-size: 11.5px; color: var(--mut);
background: #1a2417; border-top: 1px solid #d6ff3d33; padding: 6px 14px; }
.bcnrbar .breg { font-size: 10px; letter-spacing: .03em; padding: 1px 7px; border-radius: 999px;
background: rgba(214,255,61,.14); color: #d6ff3d; border: 1px solid #d6ff3d33; }
.bcnrbar b { color: #fff; }
.bcnrbar b { color: var(--ink); }
.bcnrbar .bopen { margin-left: auto; padding: 4px 11px; border-radius: 7px; border: 1px solid #d6ff3d55;
background: rgba(214,255,61,.12); color: #eaffb0; cursor: pointer; font-size: 11.5px; }
.bcnrbar .bopen:hover { background: rgba(214,255,61,.22); }
.bcnrbar .bdismiss { color: #8b98a9; text-decoration: none; } .bcnrbar .bdismiss:hover { color: #fff; }
.bcnrbar .bdismiss { color: var(--dim); text-decoration: none; } .bcnrbar .bdismiss:hover { color: var(--ink); }
</style></head>
<body>
<div class="tabs" id="tabs"></div>
@ -99,9 +116,10 @@
<span class="reg" id="reg" hidden></span>
<button class="star" id="star" title="Save this page"></button>
</div>
<span class="engine" title="Search engine (type a search in the address bar)">
<span class="mag">🔍</span><select id="engine"></select>
</span>
<div class="searchbox">
<select id="engine" class="engine-sel" title="Choose search engine"></select>
<input id="search" placeholder="Search" spellcheck="false">
</div>
<button class="tor" id="tor" title="Route traffic through Tor">🧅 Tor: Off</button>
<button class="logo" id="logo" title="Settings"><span>⛓ Theseus</span><span class="gear"></span></button>
</div>
@ -127,6 +145,7 @@
function goURL() { const v = $("url").value.trim(); if (v) T.navigate(v); }
$("url").addEventListener("keydown", (e) => { if (e.key === "Enter") goURL(); });
$("search").addEventListener("keydown", (e) => { if (e.key === "Enter") { const q = $("search").value.trim(); if (q) { T.search(q); $("search").value = ""; } } });
$("back").onclick = () => T.back();
$("fwd").onclick = () => T.forward();
$("reload").onclick = () => T.reload();
@ -142,6 +161,8 @@
sel.innerHTML = d.engines.map((e) => `<option value="${e.id}">${e.name.replace(/</g,"&lt;")}</option>`).join("")
+ `<option disabled>──────────</option><option value="__manage"> Add / edit engines…</option>`;
sel.value = d.current;
const cur = d.engines.find((e) => e.id === d.current);
$("search").placeholder = cur ? "Search with " + cur.name : "Search";
}
$("engine").onchange = () => {
const v = $("engine").value;

View file

@ -2,7 +2,7 @@
<html>
<head><meta charset="utf-8"><title>Theseus</title>
<style>
:root { color-scheme: dark; }
:root { color-scheme: light dark; }
* { box-sizing: border-box; }
body { margin: 0; min-height: 100vh; font-family: system-ui, sans-serif;
background: radial-gradient(1000px 500px at 50% -10%, #16202e, #0b0e14);
@ -30,6 +30,16 @@
.b-srv { background: #4fd1a522; color: #4fd1a5; }
footer { color: #47505f; font-size: 12px; padding-bottom: 2rem; text-align: center; }
footer b { color: #6b7688; }
/* light theme (placed last so it wins over the dark base rules) */
@media (prefers-color-scheme: light) {
body { background: radial-gradient(1000px 500px at 50% -10%, #dce6f2, #eef0f4); color: #1a1f28; }
h1 .g { color: #4a8a00; }
.tag { color: #4a5262; } .hint { color: #8a93a2; }
input { background: #ffffff; border-color: rgba(0,0,0,.14); color: #1a1f28; }
.card { background: #ffffff; border-color: rgba(0,0,0,.10); }
.card:hover { background: #f4f7fb; } .card .n { color: #1a1f28; } .card .d { color: #4a5262; }
footer { color: #8a93a2; } footer b { color: #4a5262; }
}
</style></head>
<body>
<div class="hero">

39
main.js
View file

@ -4,7 +4,7 @@
// h, Sia s3, direct ip, redirect u). Tabs, nav controls, a search box, a home
// page, and optional Tor onion routing. No system daemon; the app is the trust
// boundary.
const { app, BrowserWindow, WebContentsView, ipcMain, protocol, session, Menu, clipboard } = require("electron");
const { app, BrowserWindow, WebContentsView, ipcMain, protocol, session, Menu, clipboard, nativeTheme } = require("electron");
const path = require("path");
const http = require("http");
const https = require("https");
@ -26,13 +26,10 @@ const RESOLVER = app.isPackaged
const SEARCH_ENGINES = {
duckduckgo: { name: "DuckDuckGo", url: (q) => "https://duckduckgo.com/?q=" + encodeURIComponent(q) },
google: { name: "Google", url: (q) => "https://www.google.com/search?q=" + encodeURIComponent(q) },
bing: { name: "Bing", url: (q) => "https://www.bing.com/search?q=" + encodeURIComponent(q) },
yandex: { name: "Yandex", url: (q) => "https://yandex.com/search/?text=" + encodeURIComponent(q) },
brave: { name: "Brave", url: (q) => "https://search.brave.com/search?q=" + encodeURIComponent(q) },
bing: { name: "Bing", url: (q) => "https://www.bing.com/search?q=" + encodeURIComponent(q) },
startpage: { name: "Startpage", url: (q) => "https://www.startpage.com/sp/search?query=" + encodeURIComponent(q) },
ecosia: { name: "Ecosia", url: (q) => "https://www.ecosia.org/search?q=" + encodeURIComponent(q) },
mojeek: { name: "Mojeek", url: (q) => "https://www.mojeek.com/search?q=" + encodeURIComponent(q) },
presearch: { name: "Presearch", url: (q) => "https://presearch.com/search?q=" + encodeURIComponent(q) },
yandex: { name: "Yandex", url: (q) => "https://yandex.com/search/?text=" + encodeURIComponent(q) },
};
// Built-in + user-added, as flat metadata for the pickers.
function allEngines() {
@ -56,8 +53,8 @@ const GATEWAY = "https://navigate.st";
// DUAL — real ICANN TLDs we ALSO offer on BCNR. These never hijack the real
// web: the ICANN site loads normally, and if a BCNR name also exists
// we surface a passive "also on BCNR" switch (see navigateTab).
const BNS_NATIVE_TLDS = new Set(["bch", "p2p", "bit", "nav", "x"]);
const BNS_DUAL_TLDS = new Set(["de", "dev", "ltd"]);
const BNS_NATIVE_TLDS = new Set(["bch", "p2p", "bit", "nav", "x", "sia", "dgb", "nt"]);
const BNS_DUAL_TLDS = new Set(["de", "dev", "ltd", "ru"]);
const REGISTRY = "BCNR"; // user-facing registry label (Bitcoin Cash Name Registry)
const tldOf = (host) => {
const h = String(host).toLowerCase().replace(/\.$/, "");
@ -85,7 +82,7 @@ function looksLikeUrl(q) {
// ---- persistent user settings (userData/settings.json) ----
const SETTINGS_DEFAULTS = {
webrtcProtect: true, // reduce WebRTC IP leaks even without Tor (Tor strengthens it)
webrtcMode: "public_only", // WebRTC IP policy: default | public_only | public_private | disable_udp
blockCamera: true, // deny camera by default (also hides camera labels from fingerprinting)
blockMicrophone: true, // deny microphone by default (also hides mic labels)
restoreSession: true, // reopen last session's tabs on launch
@ -96,7 +93,13 @@ const SETTINGS_DEFAULTS = {
locationMode: "hide", locationLat: "40.7128", locationLon: "-74.0060", // coords for manual (default: deny)
searchEngine: "duckduckgo",// default search engine (built-in id or a custom id)
customEngines: [], // user-added: [{ id, name, url-with-%s }]
theme: "dark", // dark | light | system — drives prefers-color-scheme in all views
};
// Applying the theme via nativeTheme.themeSource makes prefers-color-scheme update
// in every renderer (chrome, settings, popover, page views) with no per-view IPC.
function applyTheme() {
try { nativeTheme.themeSource = ["dark", "light", "system"].includes(settings.theme) ? settings.theme : "dark"; } catch {}
}
// Auto decoys used by "spoof" mode (plausible but not the user's real values).
const SPOOF = { tz: "America/New_York", lang: "en-US", lat: 40.7128, lon: -74.0060 };
let settings = { ...SETTINGS_DEFAULTS };
@ -115,10 +118,18 @@ function loadBookmarks() { try { if (fs.existsSync(bookmarksFile())) bookmarks =
function saveBookmarks() { try { fs.writeFileSync(bookmarksFile(), JSON.stringify(bookmarks, null, 2)); } catch (e) { console.error("bookmarks save failed:", e.message); } }
function emitBookmarks() { try { chrome?.webContents.send("bookmarks", bookmarks); } catch {} }
function emitEngines() { try { chrome?.webContents.send("engines", { engines: allEngines(), current: settings.searchEngine }); } catch {} }
// WebRTC IP-handling policy: protect by default (independent of Tor), strongest under Tor.
// WebRTC IP-handling policy — the same control the "WebRTC Network Limiter"
// Chrome extension provides, done natively (that extension's chrome.privacy API
// isn't available in Electron, and this is more reliable). Tor forces the strongest.
const WEBRTC_POLICIES = {
default: "default", // allow all (may expose local IP)
public_only: "default_public_interface_only", // only the default public interface
public_private: "default_public_and_private_interfaces",
disable_udp: "disable_non_proxied_udp", // strongest (only proxied UDP)
};
function webrtcPolicy() {
if (!settings.webrtcProtect) return "default";
return torState === "on" ? "disable_non_proxied_udp" : "default_public_interface_only";
if (torState === "on") return "disable_non_proxied_udp";
return WEBRTC_POLICIES[settings.webrtcMode] || "default_public_interface_only";
}
// ---- anti-fingerprinting: timezone + language + location ----
// Show/Hide/Spoof/Manual. Effective override, or null = "show" (real value).
@ -695,7 +706,8 @@ ipcMain.handle("bookmark-remove", (_e, url) => {
ipcMain.handle("settings-get", () => settings);
ipcMain.handle("settings-set", (_e, key, val) => {
if (key in SETTINGS_DEFAULTS) { settings[key] = val; saveSettings(); }
if (key === "webrtcProtect") applyWebRTCPolicy();
if (key === "webrtcMode") applyWebRTCPolicy();
if (key === "theme") applyTheme();
if (key === "backgroundThrottle") applyThrottle();
if (["timezoneMode", "timezoneValue", "languageMode", "languageValue",
"locationMode", "locationLat", "locationLon"].includes(key)) { applyFingerprintAll(); applyAcceptLanguage(); }
@ -719,6 +731,7 @@ if (!process.env.THESEUS_NO_AUTOSTART) {
app.whenReady().then(() => {
Menu.setApplicationMenu(null); // drop the native File/Edit/View/Help menu bar
loadSettings();
applyTheme();
loadBookmarks();
applyPermissions();
applyAcceptLanguage();

View file

@ -1,7 +1,7 @@
<!doctype html>
<html><head><meta charset="utf-8">
<style>
:root { color-scheme: dark; font-family: system-ui, sans-serif; }
:root { color-scheme: light dark; font-family: system-ui, sans-serif; }
html, body { margin: 0; background: transparent; }
.card { display: flex; flex-direction: column; background: #1c222c;
border: 1px solid #ffffff26; border-radius: 8px; box-shadow: 0 12px 34px #000c; overflow: hidden; color: #e7eaf1; }
@ -21,6 +21,15 @@
.details { border-top: 1px solid #ffffff12; padding: 11px 16px 13px; display: grid; grid-template-columns: auto 1fr; gap: 7px 14px; font-size: 12px; }
.details .k { color: #7f8aa0; } .details .v { color: #e7eaf1; }
.details .v code { font-family: ui-monospace, monospace; color: #bfeae4; word-break: break-all; }
/* light theme (placed last so it wins over the dark base rules) */
@media (prefers-color-scheme: light) {
.card { background: #ffffff; border-color: rgba(0,0,0,.15); color: #1a1f28; }
.close { color: #697280; } .close:hover { background: rgba(0,0,0,.06); }
.hero .host { color: #3c4453; } .hero .sub { color: #697280; }
.details { border-top-color: rgba(0,0,0,.10); } .details .k { color: #7b8494; } .details .v { color: #1a1f28; }
.details .v code { color: #0a7d73; }
.hero.neutral .ico svg { stroke: #697280; } .hero.neutral .ico .bdy { fill: #697280; }
}
</style></head>
<body>
<div class="card">

View file

@ -2,7 +2,7 @@
<html lang="en">
<head><meta charset="utf-8"><title>Theseus — Settings</title>
<style>
:root{ color-scheme: dark; --bg:#0b0e14; --panel:#141a24; --line:rgba(255,255,255,.09);
:root{ color-scheme: light dark; --bg:#0b0e14; --panel:#141a24; --line:rgba(255,255,255,.09);
--ink:#e7eaf1; --mut:#8b98a9; --dim:#5e6678; --acid:#d6ff3d; }
*{box-sizing:border-box}
body{margin:0;height:100vh;background:var(--bg);color:var(--ink);font:15px/1.6 system-ui,-apple-system,Segoe UI,Roboto,sans-serif}
@ -45,6 +45,16 @@
.sw input:checked + .track{background:rgba(214,255,61,.25);border-color:#d6ff3d55}
.sw input:checked + .track .knob{transform:translateX(20px);background:var(--acid)}
.note{color:var(--dim);font-size:12.5px;margin-top:1.4rem;border-top:1px solid var(--line);padding-top:1rem}
/* light theme (placed last so these win over the dark base rules) */
@media (prefers-color-scheme: light){
:root{ --bg:#eef0f4; --panel:#ffffff; --line:rgba(0,0,0,.11); --ink:#1a1f28; --mut:#4a5262; --dim:#7b8494; }
.side{ background:#f6f8fb; }
.side a:hover{ background:rgba(0,0,0,.05); }
select, .ctl input, .addeng input{ background:#f1f3f7; }
.track{ background:#c8cfdb; }
.knob{ background:#8a93a2; }
code, .ceng{ background:#eef1f6; }
}
</style></head>
<body>
<div class="app">
@ -60,6 +70,10 @@
<section id="general">
<h1>General</h1>
<p class="lede">Changes apply immediately and are saved for next time.</p>
<div class="row">
<div class="txt"><div class="t">Appearance</div><div class="d">Light, dark, or follow your system theme.</div></div>
<div class="ctl"><select id="theme"><option value="dark">Dark</option><option value="light">Light</option><option value="system">System</option></select></div>
</div>
<div class="row">
<div class="txt"><div class="t">Search engine</div><div class="d">Used when you type a search into the address bar.</div></div>
<div class="ctl"><select id="searchEngine"></select></div>
@ -93,8 +107,14 @@
<h1>Privacy</h1>
<p class="lede">Reduce tracking and lock down device access.</p>
<div class="row">
<div class="txt"><div class="t">WebRTC leak protection</div><div class="d">Stops sites from discovering your real IP through WebRTC. Strongest with Tor on.</div></div>
<label class="sw"><input type="checkbox" id="webrtcProtect"><span class="track"><span class="knob"></span></span></label>
<div class="txt"><div class="t">WebRTC IP policy</div>
<div class="d">Controls which IP addresses WebRTC may reveal — the same thing the "WebRTC Network Limiter" extension does, built in. <b>Public interface only</b> hides your local IP; <b>Disable non-proxied UDP</b> is strongest. Tor forces the strongest automatically.</div></div>
<div class="ctl"><select id="webrtcMode">
<option value="default">Allow all (default)</option>
<option value="public_only">Public interface only</option>
<option value="public_private">Public + private interfaces</option>
<option value="disable_udp">Disable non-proxied UDP</option>
</select></div>
</div>
<div class="row">
<div class="txt"><div class="t">Block camera</div><div class="d">Deny camera by default — also hides its name from fingerprinting.</div></div>
@ -145,7 +165,7 @@
for (const s of sections) document.getElementById(s).hidden = (s !== a.dataset.sec);
});
const TOGGLES = ["restoreSession", "backgroundThrottle", "webrtcProtect", "blockCamera", "blockMicrophone"];
const TOGGLES = ["restoreSession", "backgroundThrottle", "blockCamera", "blockMicrophone"];
C.get().then((s) => {
for (const k of TOGGLES) {
const el = document.getElementById(k); if (!el) continue;
@ -166,6 +186,14 @@
list.querySelectorAll(".cx").forEach((b) => b.onclick = () => C.removeEngine(b.dataset.id).then(renderEngines));
}
sel.onchange = () => C.set("searchEngine", sel.value);
// appearance (theme)
const th = document.getElementById("theme");
th.value = s.theme || "dark";
th.onchange = () => C.set("theme", th.value);
// WebRTC IP policy
const wm = document.getElementById("webrtcMode");
wm.value = s.webrtcMode || "public_only";
wm.onchange = () => C.set("webrtcMode", wm.value);
document.getElementById("engAdd").onclick = () => {
const name = document.getElementById("engName").value.trim();
const url = document.getElementById("engUrl").value.trim();