theseus/settings.html
Local Dev 75e468ae39 Theseus UX polish: BCDN/BCNR terminology + soft-mode prompt redesign
Terminology (user-facing labels now match the product/registry distinction):
- BCDN = Bitcoin Cash Domain Names (the product — what you view)
- BCNR = Bitcoin Cash Name Registry (the on-chain system that backs it)
Chrome badge, popover, Settings section and 'Open with…' prompt all show BCDN
for a resolved name. BCNR stays only where the registry itself is the subject.

'Open with…' prompt (collision.html):
- Larger window (640x520, was 480x340)
- Radio-select pattern with explicit Open / Cancel buttons (was auto-fire on click)
- Enter = Open, Esc = Cancel, keyboard-first
- Body copy per operator spec (no product-name-in-parens; blockchain-generic)
- Card titles kept short: 'BCNR / BCDN' + 'ICANN / IANA'

Address-bar chip:
- Says BCDN (not BCNR) for on-chain names; ICANN for web
- Dropped the '·.tld' suffix — TLD is already in the URL bar
- On collision candidates: shows BOTH chips (BCDN | ICANN) with active
  highlighted; clicking the inactive one flips the tab (via collision-switch)

Settings section renamed Naming → Registries. Copy rewritten in the same
BCDN/BCNR frame; policy labels are now 'BCDN first' / 'ICANN first' / 'Ask
each time'.

Passive 'also on BCNR' bar copy updated to BCDN.

Verified: all preload/settings/main/chrome JS + HTML parse; no secrets in
staging.
2026-08-02 13:02:47 +02:00

509 lines
34 KiB
HTML
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.

<!doctype html>
<html lang="en">
<head><meta charset="utf-8"><title>Theseus — Settings</title>
<style>
:root{ --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}
.app{display:flex;height:100vh}
/* left sidebar menu */
.side{flex:none;width:220px;background:#0e131c;border-right:1px solid var(--line);padding:20px 12px;display:flex;flex-direction:column;gap:2px}
.brand{font-weight:700;color:var(--acid);font-size:15px;padding:4px 12px 16px}
.side a{display:block;padding:9px 12px;border-radius:8px;color:var(--mut);text-decoration:none;font-size:14px;cursor:pointer}
.side a:hover{background:#ffffff0a;color:var(--ink)}
.side a.active{background:rgba(214,255,61,.12);color:var(--acid)}
/* content */
.content{flex:1;overflow-y:auto;padding:2.4rem 2.4rem 4rem}
section{max-width:640px}
h1{font-size:1.4rem;margin:0 0 .2rem}
.lede{color:var(--mut);margin:0 0 1.8rem;font-size:13.5px}
h2.sub{font-size:11.5px;letter-spacing:.14em;text-transform:uppercase;color:var(--dim);margin:1.8rem 0 .2rem;border-top:1px solid var(--line);padding-top:1.4rem}
.subd{color:var(--mut);margin:0 0 1rem;font-size:13px}
.row{display:flex;align-items:center;gap:16px;background:var(--panel);border:1px solid var(--line);
border-radius:12px;padding:14px 18px;margin-bottom:10px}
.row .txt{flex:1}
.row .t{font-weight:600}
.row .d{color:var(--mut);font-size:13px;margin-top:2px}
/* control column — mode select + optional value field on the same row so the
dropdown menus don't get cut off underneath, wraps only when narrow */
.ctl{display:flex;flex-direction:row;flex-wrap:wrap;gap:6px;align-items:center;justify-content:flex-end;flex:none;max-width:60%}
.ctl .coords{display:flex;gap:6px}
select,.ctl input{background:#1b2330;color:var(--ink);border:1px solid var(--line);border-radius:8px;padding:7px 10px;font-size:13px;outline:none;min-width:140px}
select:focus,.ctl input:focus{border-color:#4b7bec}
.ctl input{width:170px} .ctl .coords input{width:92px;min-width:auto}
/* Chromium's native <select> popup uses the page's color-scheme; when it's
"light dark" (both accepted) Chromium picks by the OS, so a dark-theme app
on a light OS shows a light popup. main.js's applyTheme() maps to
nativeTheme.themeSource, which drives prefers-color-scheme — so pinning
color-scheme via that media query keeps the popup in sync automatically. */
:root { color-scheme: dark; }
@media (prefers-color-scheme: light) { :root { color-scheme: light; } }
/* Explicit option styling — Chromium respects it in the popup on Windows. */
select option { background: #1b2330; color: var(--ink); }
@media (prefers-color-scheme: light) { select option { background: #f1f3f7; color: #1a1f28; } }
code{font-family:ui-monospace,monospace;font-size:12px;background:#0e131b;border:1px solid var(--line);border-radius:5px;padding:1px 5px;color:#bfeae4}
.addeng{display:flex;gap:6px} .addeng input{flex:1;background:#1b2330;color:var(--ink);border:1px solid var(--line);border-radius:8px;padding:8px 10px;font-size:13px;outline:none}
.addeng input:focus{border-color:#4b7bec}
.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)}
/* segmented control — small toggle used elsewhere (kept for reuse) */
.segseg{display:inline-flex;background:#10151f;border:1px solid var(--line);border-radius:8px;padding:2px;gap:2px}
.segseg .seg{background:transparent;color:var(--mut);border:none;border-radius:6px;padding:6px 12px;font-size:13px;cursor:pointer;font:inherit;line-height:1.2}
.segseg .seg:hover{color:var(--ink)}
.segseg .seg.on{background:#1c2432;color:var(--ink);box-shadow:inset 0 0 0 1px var(--line)}
/* theme cards — three visual previews (System / Light / Dark) */
.themeCards{display:flex;gap:14px;margin:6px 0 4px;flex-wrap:wrap}
.themeCards .tc{background:transparent;border:2px solid var(--line);border-radius:10px;padding:10px;
display:flex;flex-direction:column;align-items:center;gap:8px;cursor:pointer;color:var(--ink);
font:inherit;font-size:13px;min-width:150px;transition:border-color .12s}
.themeCards .tc:hover{border-color:#4b7bec55}
.themeCards .tc.on{border-color:#4b7bec;box-shadow:0 0 0 1px #4b7bec inset}
.themeCards .mock{width:130px;height:82px;border-radius:6px;overflow:hidden;display:block;position:relative;
border:1px solid rgba(255,255,255,.08)}
.themeCards .mock .mm-chrome{position:absolute;left:0;right:0;top:0;height:22px;display:block}
.themeCards .mock .mm-chrome::before{content:"";position:absolute;left:8px;top:6px;width:8px;height:8px;border-radius:50%;background:#f6768a}
.themeCards .mock .mm-chrome::after {content:"";position:absolute;left:22px;top:6px;width:8px;height:8px;border-radius:50%;background:#f6c15c;box-shadow:14px 0 0 #6ec27d}
.themeCards .mock .mm-body{position:absolute;left:0;right:0;top:22px;bottom:0;display:block}
.themeCards .mock-light .mm-chrome{background:#e9ecf2}
.themeCards .mock-light .mm-body {background:#ffffff;background-image:linear-gradient(#0000000c 1px,transparent 1px);background-size:100% 12px;background-position:0 10px}
.themeCards .mock-dark .mm-chrome{background:#141b28}
.themeCards .mock-dark .mm-body {background:#0b0e14;background-image:linear-gradient(#ffffff10 1px,transparent 1px);background-size:100% 12px;background-position:0 10px}
.themeCards .mock-system .mm-chrome{background:linear-gradient(90deg,#141b28 0 50%,#e9ecf2 50% 100%)}
.themeCards .mock-system .mm-body{background:linear-gradient(90deg,#0b0e14 0 50%,#ffffff 50% 100%)}
.themeCards .tc-label{font-weight:500;color:var(--ink)}
.polrow{display:flex;align-items:center;gap:10px;background:#10151f;border:1px solid var(--line);border-radius:8px;padding:8px 12px;font-size:13px;cursor:pointer}
.polrow:hover{background:#141c28}
.polrow input{accent-color:#d6ff3d}
.pmuted{color:var(--mut)}
.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}
/* engine checklist */
.eng{display:flex;align-items:center;gap:10px;background:#10151f;border:1px solid var(--line);border-radius:8px;padding:6px 10px;margin-bottom:6px;font-size:13px}
.eng .eic{width:18px;height:18px;flex:none;display:grid;place-items:center}
.eng .eic .ei{width:16px;height:16px;border-radius:3px} .eng .eic .es{font-size:14px}
.eng .enm{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.eng .cx{cursor:pointer;color:var(--dim);border:none;background:transparent;font-size:13px} .eng .cx:hover{color:#f6768a}
.eng{cursor:default}
.eng .grip{flex:none;color:var(--dim);cursor:grab;font-size:14px;line-height:1;padding:0 2px;user-select:none}
.eng .grip:active{cursor:grabbing}
.eng.dragging{opacity:.45}
.eng.over{border-color:var(--acid);box-shadow:0 -2px 0 var(--acid) inset}
.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}
/* 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}
.engcat .cat:hover{background:#141b26}
.engcat .cat .eic{width:18px;height:18px;flex:none;display:grid;place-items:center}
.engcat .cat .eic .ei{width:16px;height:16px;border-radius:3px} .engcat .cat .eic .es{font-size:14px}
.engcat .cat .enm{flex:1}
.engcat .cat .kind{font-size:10.5px;letter-spacing:.05em;color:var(--dim);padding:1px 6px;border:1px solid var(--line);border-radius:999px}
.engcat .cat .add{background:transparent;border:1px solid var(--line);color:var(--ink);border-radius:6px;padding:4px 10px;font-size:12px;cursor:pointer}
.engcat .cat .add:hover{background:rgba(214,255,61,.14);border-color:#d6ff3d55;color:#eaffb0}
.engcat .cempty2{color:var(--dim);font-size:12.5px;padding:6px 8px}
.sw.sm{width:38px;height:22px} .sw.sm .knob{width:15px;height:15px} .sw.sm input:checked + .track .knob{transform:translateX(16px)}
.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}
.cempty{color:var(--dim);font-size:12.5px}
/* toggle */
.sw{position:relative;width:46px;height:26px;flex:none;cursor:pointer}
.sw input{opacity:0;width:0;height:0}
.track{position:absolute;inset:0;background:#2b3444;border:1px solid var(--line);border-radius:999px;transition:.15s}
.knob{position:absolute;top:3px;left:3px;width:18px;height:18px;border-radius:50%;background:#8b98a9;transition:.15s}
.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, .eng{ background:#eef1f6; }
}
</style></head>
<body>
<div class="app">
<nav class="side">
<div class="brand">⛓ Theseus</div>
<a data-sec="general" class="active">General</a>
<a data-sec="search">Search</a>
<a data-sec="naming">Registries</a>
<a data-sec="performance">Performance</a>
<a data-sec="privacy">Privacy</a>
</nav>
<div class="content">
<!-- GENERAL -->
<section id="general">
<h1>General</h1>
<p class="lede">Changes apply immediately and are saved for next time.</p>
<h2 class="sub" style="border-top:0;padding-top:0;margin-top:0">Startup</h2>
<div class="row">
<div class="txt"><div class="t">Open previous windows and tabs</div><div class="d">Restore the tabs from your last session when Theseus starts.</div></div>
<label class="sw"><input type="checkbox" id="restoreSession"><span class="track"><span class="knob"></span></span></label>
</div>
<h2 class="sub">Appearance</h2>
<p class="subd">Choose a theme for the browser. <b>System</b> follows your operating system's light/dark setting.</p>
<div class="themeCards" id="theme" role="radiogroup" aria-label="Theme">
<button type="button" class="tc" data-val="system" role="radio" aria-checked="false">
<span class="mock mock-system"><span class="mm-chrome"></span><span class="mm-body"></span></span>
<span class="tc-label">System</span>
</button>
<button type="button" class="tc" data-val="light" role="radio" aria-checked="false">
<span class="mock mock-light"><span class="mm-chrome"></span><span class="mm-body"></span></span>
<span class="tc-label">Light</span>
</button>
<button type="button" class="tc" data-val="dark" role="radio" aria-checked="false">
<span class="mock mock-dark"><span class="mm-chrome"></span><span class="mm-body"></span></span>
<span class="tc-label">Dark</span>
</button>
</div>
</section>
<!-- SEARCH -->
<section id="search" hidden>
<h1>Search</h1>
<p class="lede">Pick what your address bar and the toolbar dropdown search with.</p>
<div class="row">
<div class="txt"><div class="t">Default search engine</div><div class="d">Used when you type into the address bar.</div></div>
<div class="ctl"><select id="searchEngine"></select></div>
</div>
<div class="row" style="flex-direction:column;align-items:stretch;gap:10px">
<div class="txt" style="display:flex;align-items:center;justify-content:space-between;gap:12px">
<div>
<div class="t">Additional search engines</div>
<div class="d">These appear in the toolbar dropdown. Drag to reorder. Toggle off to move an engine back to the catalog.</div>
</div>
<button id="engAddBtn" class="btn" type="button">+ Add search engine</button>
</div>
<div id="engineList"></div>
<!-- Catalog: hidden until "Add" is clicked. Shows built-in engines the user
hasn't enabled + a custom-URL form. -->
<div id="engineCatalog" class="engcat" hidden>
<div class="ehdr">Add from catalog</div>
<div id="catalogList"></div>
<div class="ehdr">Or add a custom URL</div>
<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="engUrl" placeholder="https://example.com/search?q=%s">
<button id="engAdd" class="btn">Add</button>
</div>
</div>
</div>
</section>
<!-- NAMING -->
<section id="naming" hidden>
<h1>Registries</h1>
<p class="lede">How Theseus picks between <b>BCDN</b> (Bitcoin Cash Domain Names, on-chain) and <b>ICANN</b> (traditional DNS) when a name exists in both registries.</p>
<div class="row" style="flex-direction:column;align-items:stretch;gap:10px">
<div class="txt"><div class="t">Collision policy</div>
<div class="d">A collision only happens when a BCDN name is registered under a TLD that ICANN also serves (e.g. <code>.de</code>). BCNR-native TLDs (<code>.bch</code>, <code>.p2p</code>, …) never conflict — they only exist on BCDN.</div></div>
<div id="policyList" style="display:flex;flex-direction:column;gap:6px">
<label class="polrow"><input type="radio" name="collisionPolicy" value="bcnr-first"><span><b>BCDN first</b> <span class="pmuted">— BCDN wins conflicts; falls back to ICANN for anything BCDN doesn't have.</span></span></label>
<label class="polrow"><input type="radio" name="collisionPolicy" value="icann-first"><span><b>ICANN first</b> <span class="pmuted">— ICANN wins conflicts; BCDN fills gaps for non-ICANN TLDs.</span></span></label>
<label class="polrow"><input type="radio" name="collisionPolicy" value="soft"><span><b>Ask each time</b> <span class="pmuted">— an "Open with…" prompt on conflict, remembered per name or per TLD.</span></span></label>
</div>
</div>
<div class="row" style="flex-direction:column;align-items:stretch;gap:8px">
<div class="txt"><div class="t">Remembered choices</div>
<div class="d">"Always use …" picks you made from the switcher or the prompt. Reset them to be asked again.</div></div>
<div id="colSummary" class="pmuted" style="font-size:12.5px"></div>
<div><button id="resetCollisions" class="btn">Reset remembered choices</button></div>
</div>
</section>
<!-- PERFORMANCE -->
<section id="performance" hidden>
<h1>Performance</h1>
<p class="lede">Keep Theseus light on resources.</p>
<div class="row">
<div class="txt"><div class="t">Throttle inactive tabs</div><div class="d">Background and inactive tabs use far less CPU. Recommended.</div></div>
<label class="sw"><input type="checkbox" id="backgroundThrottle"><span class="track"><span class="knob"></span></span></label>
</div>
</section>
<!-- PRIVACY -->
<section id="privacy" hidden>
<h1>Privacy</h1>
<p class="lede">Reduce tracking and lock down device access.</p>
<div class="row">
<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>
<label class="sw"><input type="checkbox" id="blockCamera"><span class="track"><span class="knob"></span></span></label>
</div>
<div class="row">
<div class="txt"><div class="t">Block microphone</div><div class="d">Deny microphone by default — also hides its name from fingerprinting.</div></div>
<label class="sw"><input type="checkbox" id="blockMicrophone"><span class="track"><span class="knob"></span></span></label>
</div>
<div class="row">
<div class="txt"><div class="t">Hide media devices</div><div class="d">Blank the labels and IDs of all cameras, microphones <b>and speakers</b> from <code>enumerateDevices()</code> — closes a WebRTC fingerprinting leak, like Firefox.</div></div>
<label class="sw"><input type="checkbox" id="hideMediaDevices"><span class="track"><span class="knob"></span></span></label>
</div>
<h2 class="sub">Anti-fingerprinting</h2>
<p class="subd">For each: <b>Show</b> (real), <b>Hide</b> (neutral value), <b>Spoof</b> (a decoy), or <b>Manual</b> (set your own).</p>
<div class="row">
<div class="txt"><div class="t">Timezone</div><div class="d">What sites read via JavaScript (Intl / Date).</div></div>
<div class="ctl">
<select id="timezoneMode"><option value="show">Show real</option><option value="hide">Hide (UTC)</option><option value="spoof">Spoof (auto)</option><option value="manual">Manual…</option></select>
<input id="timezoneValue" list="tzList" placeholder="pick or type e.g. Europe/Berlin" hidden>
<datalist id="tzList">
<option value="UTC"><option value="Europe/London"><option value="Europe/Berlin"><option value="Europe/Paris"><option value="Europe/Moscow">
<option value="America/New_York"><option value="America/Chicago"><option value="America/Los_Angeles"><option value="America/Sao_Paulo">
<option value="Asia/Tokyo"><option value="Asia/Shanghai"><option value="Asia/Kolkata"><option value="Asia/Dubai"><option value="Australia/Sydney"><option value="Africa/Nairobi">
</datalist>
</div>
</div>
<div class="row">
<div class="txt"><div class="t">Language</div><div class="d">navigator.language and the Accept-Language header. <b>Spoof</b> reports a major world language; <b>Manual</b> lets you type any locale.</div></div>
<div class="ctl">
<select id="languageMode"><option value="show">Show real</option><option value="hide">Hide (en-US)</option><option value="spoof">Spoof (choose)</option><option value="manual">Manual…</option></select>
<select id="languageSpoof" hidden>
<option value="en-US">English</option>
<option value="zh-CN">Chinese (中文)</option>
<option value="es-ES">Spanish (Español)</option>
<option value="hi-IN">Hindi (हिन्दी)</option>
<option value="ar">Arabic (العربية)</option>
<option value="pt-BR">Portuguese (Português)</option>
<option value="ru-RU">Russian (Русский)</option>
<option value="fr-FR">French (Français)</option>
<option value="de-DE">German (Deutsch)</option>
<option value="ja-JP">Japanese (日本語)</option>
</select>
<input id="languageValue" list="langList" placeholder="pick or type e.g. it-IT" hidden>
<datalist id="langList">
<option value="en-US"><option value="en-GB"><option value="zh-CN"><option value="es-ES"><option value="hi-IN"><option value="ar">
<option value="pt-BR"><option value="ru-RU"><option value="fr-FR"><option value="de-DE"><option value="ja-JP"><option value="it-IT"><option value="ko-KR"><option value="nl-NL"><option value="tr-TR">
</datalist>
</div>
</div>
<div class="row">
<div class="txt"><div class="t">Location</div><div class="d"><b>Hide</b> denies geolocation; <b>Spoof</b> reports a chosen region; <b>Manual</b> reports exact coordinates.</div></div>
<div class="ctl">
<select id="locationMode"><option value="show">Show real</option><option value="hide">Hide (block)</option><option value="spoof">Spoof (region)</option><option value="manual">Manual…</option></select>
<select id="locationRegion" hidden>
<option value="europe">Europe</option>
<option value="asia">Asia</option>
<option value="north_america">North America</option>
<option value="south_america">South America</option>
<option value="africa">Africa</option>
<option value="middle_east">Middle East</option>
<option value="australia">Australia</option>
</select>
<div class="coords" id="locationCoords" hidden>
<input id="locationCity" list="cityList" placeholder="pick a city…" style="width:112px">
<input id="locationLat" placeholder="lat"><input id="locationLon" placeholder="lon">
</div>
<datalist id="cityList">
<option value="London"><option value="Berlin"><option value="Paris"><option value="Moscow"><option value="New York"><option value="Los Angeles">
<option value="São Paulo"><option value="Tokyo"><option value="Shanghai"><option value="Mumbai"><option value="Dubai"><option value="Sydney"><option value="Nairobi"><option value="Singapore">
</datalist>
</div>
</div>
<div class="note">These reduce tracking and hide your IP, but a custom browser can still be fingerprinted. For maximum anonymity, use the Tor Browser.</div>
</section>
</div>
</div>
<script>
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);
});
const TOGGLES = ["restoreSession", "backgroundThrottle", "blockCamera", "blockMicrophone", "hideMediaDevices"];
C.get().then((s) => {
for (const k of TOGGLES) {
const el = document.getElementById(k); if (!el) continue;
el.checked = !!s[k];
el.addEventListener("change", () => C.set(k, el.checked));
}
// search engines: default picker + custom-engine list + add form
const sel = document.getElementById("searchEngine");
const esc = (s) => String(s || "").replace(/</g, "&lt;");
const engIcon = (e) => e.favicon
? `<img class="ei" src="${esc(e.favicon)}" onerror="this.replaceWith(Object.assign(document.createElement('span'),{className:'es',textContent:'${e.sym || "🔍"}'}))">`
: `<span class="es">${e.sym || "🔍"}</span>`;
const ENGINE_KINDS = [
{ key: "search", label: "Search engines" },
{ key: "llm", label: "AI answer engines" },
];
function renderEngines(d) {
// default engine: pick from the ENABLED engines (those shown in the dropdown)
const enabled = d.engines.filter((e) => e.enabled);
// group the dropdown too, so search and LLM don't intermix
sel.innerHTML = ENGINE_KINDS.map(({ key, label }) => {
const opts = enabled.filter((e) => (e.kind || "search") === key)
.map((e) => `<option value="${e.id}">${(e.sym ? e.sym + " " : "")}${esc(e.name)}</option>`).join("");
return opts ? `<optgroup label="${label}">${opts}</optgroup>` : "";
}).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.
const list = document.getElementById("engineList");
const rowFor = (e) => `<div class="eng" data-id="${e.id}" data-kind="${e.kind || "search"}" draggable="true">` +
`<span class="grip" title="Drag to reorder">⠿</span>` +
`<span class="eic">${engIcon(e)}</span><span class="enm">${esc(e.name)}</span>` +
(e.builtin
? `<label class="sw sm"><input type="checkbox" data-id="${e.id}" ${e.enabled ? "checked" : ""}><span class="track"><span class="knob"></span></span></label>`
: `<button class="cx" data-id="${e.id}" title="Remove">✕</button>`) + `</div>`;
list.innerHTML = ENGINE_KINDS.map(({ key, label }) => {
const rows = enabled.filter((e) => (e.kind || "search") === key).map(rowFor).join("");
if (!rows) return "";
return `<div class="ehdr">${label}</div>${rows}`;
}).join("");
// Catalog = built-in engines the user has NOT enabled, ready to add.
const cat = document.getElementById("catalogList");
if (cat) {
const off = d.engines.filter((e) => e.builtin && !e.enabled);
const catRow = (e) => `<div class="cat" data-id="${e.id}">` +
`<span class="eic">${engIcon(e)}</span>` +
`<span class="enm">${esc(e.name)}</span>` +
`<span class="kind">${(e.kind || "search") === "llm" ? "AI" : "Search"}</span>` +
`<button class="add" data-add="${e.id}">+ Add</button></div>`;
cat.innerHTML = off.length
? off.map(catRow).join("")
: `<div class="cempty2">All built-in engines are enabled. Add a custom URL below.</div>`;
cat.querySelectorAll(".add").forEach((b) => b.onclick = () => C.setEngineEnabled(b.dataset.add, true).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));
// 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).
let dragId = null;
let dragKind = null;
const sameKind = (row) => row.dataset.kind === dragKind;
list.querySelectorAll(".eng").forEach((row) => {
row.addEventListener("dragstart", (e) => { dragId = row.dataset.id; dragKind = row.dataset.kind; e.dataTransfer.effectAllowed = "move"; row.classList.add("dragging"); });
row.addEventListener("dragend", () => { row.classList.remove("dragging"); list.querySelectorAll(".eng").forEach((r) => r.classList.remove("over")); });
row.addEventListener("dragover", (e) => {
if (!sameKind(row)) { e.dataTransfer.dropEffect = "none"; return; }
e.preventDefault(); e.dataTransfer.dropEffect = "move";
if (row.dataset.id !== dragId) row.classList.add("over");
});
row.addEventListener("dragleave", () => row.classList.remove("over"));
row.addEventListener("drop", (e) => {
e.preventDefault(); row.classList.remove("over");
if (!dragId || dragId === row.dataset.id || !sameKind(row)) return;
const ids = [...list.querySelectorAll(".eng")].map((el) => el.dataset.id);
const from = ids.indexOf(dragId), to = ids.indexOf(row.dataset.id);
ids.splice(from, 1); ids.splice(to, 0, dragId);
C.setEngineOrder(ids).then(renderEngines);
});
});
}
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).
const th = document.getElementById("theme");
let themeValue = ["system", "light", "dark"].includes(s.theme) ? s.theme : "system";
const paintTheme = () => th.querySelectorAll(".tc").forEach((b) => {
const on = b.dataset.val === themeValue;
b.classList.toggle("on", on);
b.setAttribute("aria-checked", on ? "true" : "false");
});
paintTheme();
th.querySelectorAll(".tc").forEach((b) => b.onclick = () => {
themeValue = b.dataset.val; paintTheme(); C.set("theme", themeValue);
});
// 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();
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; }
C.addEngine({ name, url, sym }).then((d) => {
document.getElementById("engName").value = ""; document.getElementById("engUrl").value = ""; document.getElementById("engSym").value = "";
renderEngines(d);
});
};
// "+ Add search engine" toggles the catalog panel below the enabled list.
const catBtn = document.getElementById("engAddBtn");
const catBox = document.getElementById("engineCatalog");
if (catBtn && catBox) {
catBtn.onclick = () => {
const open = catBox.hidden;
catBox.hidden = !open;
catBtn.textContent = open ? " Hide catalog" : "+ Add search engine";
};
}
C.engines().then(renderEngines);
// anti-fingerprinting mode selectors, with value field(s) shown on "manual"
const bind = (mode, showValIf, apply) => {
const m = document.getElementById(mode);
m.value = s[mode] || "show";
apply(m.value === "manual");
m.addEventListener("change", () => { C.set(mode, m.value); apply(m.value === "manual"); });
};
const val = (id) => { const v = document.getElementById(id); v.value = s[id] ?? ""; v.addEventListener("change", () => C.set(id, v.value.trim())); return v; };
const tzV = val("timezoneValue"), lgV = val("languageValue");
val("locationLat"); val("locationLon");
bind("timezoneMode", null, (manual) => tzV.hidden = !manual);
// language: top-10 dropdown when spoofing, free-text locale when manual
const lngMode = document.getElementById("languageMode");
const lngSpoof = document.getElementById("languageSpoof");
lngSpoof.value = s.languageSpoof || "en-US";
lngSpoof.addEventListener("change", () => C.set("languageSpoof", lngSpoof.value));
const applyLng = () => { lngSpoof.hidden = lngMode.value !== "spoof"; lgV.hidden = lngMode.value !== "manual"; };
lngMode.value = s.languageMode || "show";
applyLng();
lngMode.addEventListener("change", () => { C.set("languageMode", lngMode.value); applyLng(); });
// location: region dropdown when spoofing, exact coords when manual
const locMode = document.getElementById("locationMode");
const locRegion = document.getElementById("locationRegion");
const locCoords = document.getElementById("locationCoords");
locRegion.value = s.locationRegion || "europe";
locRegion.addEventListener("change", () => C.set("locationRegion", locRegion.value));
const applyLoc = () => { locRegion.hidden = locMode.value !== "spoof"; locCoords.hidden = locMode.value !== "manual"; };
locMode.value = s.locationMode || "show";
applyLoc();
locMode.addEventListener("change", () => { C.set("locationMode", locMode.value); applyLoc(); });
// picking a city fills the exact lat/lon fields
const CITY = { "London":[51.5074,-0.1278],"Berlin":[52.52,13.405],"Paris":[48.8566,2.3522],"Moscow":[55.7558,37.6173],"New York":[40.7128,-74.006],"Los Angeles":[34.0522,-118.2437],"São Paulo":[-23.5505,-46.6333],"Tokyo":[35.6762,139.6503],"Shanghai":[31.2304,121.4737],"Mumbai":[19.076,72.8777],"Dubai":[25.2048,55.2708],"Sydney":[-33.8688,151.2093],"Nairobi":[-1.2921,36.8219],"Singapore":[1.3521,103.8198] };
document.getElementById("locationCity").addEventListener("change", (e) => {
const c = CITY[e.target.value.trim()]; if (!c) return;
const la = document.getElementById("locationLat"), lo = document.getElementById("locationLon");
la.value = c[0]; lo.value = c[1]; C.set("locationLat", String(c[0])); C.set("locationLon", String(c[1]));
});
// ---- Naming section: BCNR/ICANN collision policy + remembered choices ----
function refreshCollisions() {
C.collisionState().then((cs) => {
// pick the current radio
document.querySelectorAll('input[name="collisionPolicy"]').forEach((r) => { r.checked = (r.value === cs.policy); });
const nn = Object.keys(cs.byName || {}).length, tn = Object.keys(cs.byTld || {}).length;
const bc = (cs.bcnrTlds || []).length;
document.getElementById("colSummary").textContent =
`Remembered: ${nn} name${nn === 1 ? "" : "s"}, ${tn} TLD${tn === 1 ? "" : "s"}. `
+ `BCNR-native TLDs on chain: ${bc}.`;
}).catch(() => {});
}
refreshCollisions();
document.querySelectorAll('input[name="collisionPolicy"]').forEach((r) => {
r.addEventListener("change", () => { if (r.checked) C.setCollisionPolicy(r.value).then(refreshCollisions); });
});
document.getElementById("resetCollisions").onclick = () => {
C.resetCollisions().then(refreshCollisions);
};
});
</script>
</body>
</html>