The profile folder was Electron's default from the product name
("Theseus Navigator") and add-ons lived in addons\ under it. Now:
%APPDATA%\Theseus\extensions\ installed extensions
%APPDATA%\Theseus\extensions-data\ per-extension storage + scratch
%APPDATA%\Theseus\extensions-backups\ replaced copies
%APPDATA%\Theseus\extensions-staged\ staged updates
Both moves are one-time migrations on the first start that finds the old
layout: the profile folder is renamed (same volume, instant) or copied
when a rename is refused, with the old folder left in place in that case;
the four sub-folders are renamed before the extension host first reads
them. Nothing is deleted. THESEUS_USER_DATA still overrides everything.
The host now hands each extension its data folder as api.dataDir; the
Screenshot and PDF editor add-ons used to rebuild the old path from their
own folder for scratch files (so they recreated addons-data\ after the
move) and now use the field, with versions bumped so the bundles reseed.
1448 lines
94 KiB
HTML
1448 lines
94 KiB
HTML
<!doctype html>
|
||
<html lang="en">
|
||
<head><meta charset="utf-8"><title>Theseus — Settings</title>
|
||
<style>
|
||
/* --acid-text: color used for text/inline emphasis. In dark mode it
|
||
equals --acid (bright acid green, ~10:1 on the dark ground). Light
|
||
mode overrides to #253A49 (BCH dark navy) which is ~12:1 on white,
|
||
because the BCH-teal --acid (#0AC18E) only manages ~2.9:1 as text.
|
||
--acid still drives fills, borders, and background tints where a
|
||
brand-green splash is wanted. */
|
||
:root{ --bg:#0b0e14; --panel:#141a24; --line:rgba(255,255,255,.09);
|
||
--ink:#e7eaf1; --mut:#8b98a9; --dim:#5e6678; --acid:#d6ff3d; --acid-text:#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-text);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:rgb(from var(--acid) r g b / .12);color:var(--acid-text)}
|
||
/* 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; --acid: #0AC18E; --acid-text: #253A49; } }
|
||
/* 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 color-mix(in srgb, var(--acid) 33%, transparent);background:rgb(from var(--acid) r g b / .12);color:var(--acid-text);border-radius:8px;padding:8px 14px;font-size:13px;cursor:pointer}
|
||
.btn:hover{background:rgb(from var(--acid) r g b / .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:var(--acid)}
|
||
.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}
|
||
.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}
|
||
.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:rgb(from var(--acid) r g b / .14);border-color:color-mix(in srgb, var(--acid) 33%, transparent);color:var(--acid-text)}
|
||
.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:rgb(from var(--acid) r g b / .25);border-color:color-mix(in srgb, var(--acid) 33%, transparent)}
|
||
.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:#F8FDFF; --panel:#ffffff; --line:rgba(37,58,73,.15); --ink:#253A49; --mut:#4a5262; --dim:#7b8494; }
|
||
.side{ background:#f1f5f8; border-right-color:rgba(37,58,73,.10); }
|
||
.side a{ color:#4a5262; }
|
||
.side a:hover{ background:rgba(37,58,73,.06); color:#253A49; }
|
||
/* Every dark-hardcoded card / control needs a light equivalent. All the
|
||
#10151f / #0e131c / #1b2330 tones map to a subtle off-white so they
|
||
still read as cards against the #F8FDFF page, with rgba(37,58,73,X)
|
||
hover tints keyed to BCH dark for consistent depth. */
|
||
.polrow{ background:#f4f8fb; color:#253A49; }
|
||
.polrow:hover{ background:#eaf0f5; }
|
||
.polrow input:checked ~ span,
|
||
.polrow:has(input:checked){ background:rgb(from var(--acid) r g b / .10); border-color:color-mix(in srgb, var(--acid) 35%, transparent); }
|
||
.segseg{ background:#eef1f6; }
|
||
.segseg .seg.on{ background:#ffffff; color:#253A49; box-shadow:inset 0 0 0 1px rgba(37,58,73,.12); }
|
||
.engcat{ background:#f4f8fb; }
|
||
.engcat .cat:hover{ background:#eaf0f5; }
|
||
select, .ctl input, .addeng input{ background:#f1f5f8; color:#253A49; }
|
||
select option{ background:#ffffff; color:#253A49; }
|
||
.track{ background:#d3dae3; }
|
||
.knob{ background:#8a93a2; }
|
||
code, .ceng, .eng{ background:#eef2f6; color:#253A49; }
|
||
.themeCards .tc{ background:#ffffff; border-color:rgba(37,58,73,.15); }
|
||
.themeCards .tc:hover{ border-color:rgba(37,58,73,.30); }
|
||
.ctxmenu{ background:#ffffff; border-color:rgba(37,58,73,.15); color:#253A49; }
|
||
.ctxmenu .mi:hover{ background:rgba(37,58,73,.06); }
|
||
}
|
||
</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="passwords">Passwords</a>
|
||
<a data-sec="performance">Performance</a>
|
||
<a data-sec="privacy">Privacy</a>
|
||
<a data-sec="plugins">Plug-ins</a>
|
||
<a data-sec="addons">Extensions</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>
|
||
<div class="row" style="flex-direction:column;align-items:stretch;gap:8px">
|
||
<div class="txt">
|
||
<div class="t">Updates</div>
|
||
<div class="d">Theseus already checks the release manifest at boot and every 6h. Click below to check right now.</div>
|
||
</div>
|
||
<div id="updStatus" class="pmuted" style="font-size:12.5px">Loading…</div>
|
||
<div style="display:flex;gap:8px;flex-wrap:wrap">
|
||
<button id="updCheck" class="btn">Check for updates</button>
|
||
</div>
|
||
</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>
|
||
|
||
<h2 class="sub" style="margin-top:1.8rem">Developer tools</h2>
|
||
<p class="subd">Where the Chromium DevTools panel appears when you press <b>F12</b> or <b>Ctrl+Shift+I</b>. You can always drag it out to a separate window from inside the panel.</p>
|
||
<div class="row" style="flex-direction:column;align-items:stretch;gap:10px">
|
||
<div id="devToolsDockList" style="display:flex;flex-direction:column;gap:6px">
|
||
<label class="polrow"><input type="radio" name="devToolsDock" value="bottom"><span><b>Bottom panel</b> <span class="pmuted">— docked under the tab, like Chrome's default. Best for wide screens.</span></span></label>
|
||
<label class="polrow"><input type="radio" name="devToolsDock" value="sidebar"><span><b>Right sidebar</b> <span class="pmuted">— DevTools takes the right side. If an add-on sidebar is open, it hides while DevTools is up.</span></span></label>
|
||
<label class="polrow"><input type="radio" name="devToolsDock" value="two-sidebars"><span><b>Two sidebars</b> <span class="pmuted">— DevTools opens on the right, and the add-on sidebar stays where it is. The two share the right area.</span></span></label>
|
||
</div>
|
||
</div>
|
||
|
||
<h2 class="sub" style="margin-top:1.8rem">Registries</h2>
|
||
<p class="subd">How Theseus picks between the <b>BCNR / BCDN</b> and <b>ICANN / IANA</b>, when a name exists in both.</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 name only exists in both registries when its TLD isn't BCNR-unique (e.g. <code>.de</code>). BCNR-unique TLDs (that only exist on BCNR) never conflict.</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>
|
||
<!-- PLUG-INS -->
|
||
<section id="plugins" hidden>
|
||
<h1>Plug-ins</h1>
|
||
<p class="lede">Silent Mode components that live alongside Theseus — the system-wide resolver and the built-in wallet. Each ships with its own install / update / on-off controls.</p>
|
||
<div class="row" style="justify-content:flex-end;gap:8px"><a class="btn" style="text-decoration:none;display:inline-flex;align-items:center" href="https://theseus.x/plug-ins/" target="_blank" rel="noopener" title="Current signed versions, hashes and how plug-ins update">About plug-ins on theseus.x ↗</a></div>
|
||
<div class="row" style="flex-direction:column;align-items:stretch;gap:8px">
|
||
<div class="txt">
|
||
<div class="t">System-wide resolver — Ariadne's Thread</div>
|
||
<div class="d">
|
||
A local daemon that resolves BCDN names for <b>every browser on this machine</b>
|
||
(Chrome, Edge, Firefox, etc.), not just Theseus. Turning it off means non-Theseus
|
||
browsers stop resolving <code>.bch</code> / <code>.x</code> / other BCNR TLDs;
|
||
Theseus keeps working either way, since it has its own built-in resolver.
|
||
</div>
|
||
</div>
|
||
<div id="ariadneStatus" class="pmuted" style="font-size:12.5px">checking…</div>
|
||
<div style="display:flex;gap:8px;flex-wrap:wrap">
|
||
<button id="ariadneStart" class="btn" hidden>Turn on</button>
|
||
<button id="ariadneStop" class="btn" hidden>Turn off</button>
|
||
<button id="ariadneInstall" class="btn" hidden>Install</button>
|
||
<button id="ariadneUpdate" class="btn" hidden>Update</button>
|
||
<button id="ariadneUninstall" class="btn" hidden style="color:#f6768a;border-color:rgba(246,118,138,.35)">Uninstall</button>
|
||
<button id="ariadneRefresh" class="btn">Refresh</button>
|
||
</div>
|
||
<div id="ariadneMissing" class="pmuted" style="font-size:12px" hidden>
|
||
Ariadne's Thread isn't installed on this machine. Click <b>Install</b> above
|
||
to run the bundled installer, or grab it manually from
|
||
<a href="https://silentmode.st/tools/">silentmode.st/tools</a>.
|
||
</div>
|
||
</div>
|
||
<div class="row" style="flex-direction:column;align-items:stretch;gap:8px">
|
||
<div class="txt">
|
||
<div class="t">Built-in wallet — Aegis</div>
|
||
<div class="d">
|
||
Multi-chain wallet (BCH, BTC, TRX, ETH, SOL, SC, DGB) that ships in the box.
|
||
Bundled with Theseus, but updates are delivered <b>over-the-air</b>: new signed
|
||
versions land without a Theseus release. Runs entirely inside this browser —
|
||
never system-wide.
|
||
</div>
|
||
</div>
|
||
<div id="aegisStatus" class="pmuted" style="font-size:12.5px">Loading…</div>
|
||
<div style="display:flex;gap:8px;flex-wrap:wrap">
|
||
<button id="aegisCheck" class="btn">Check for updates</button>
|
||
<button id="aegisApply" class="btn" hidden>Restart to apply update</button>
|
||
</div>
|
||
</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. Three tiers, top to bottom:
|
||
1. curated built-ins the user hasn't enabled (tier="catalog")
|
||
2. wider bank filtered by a search box (tier="extra")
|
||
3. custom-URL form -->
|
||
<div id="engineCatalog" class="engcat" hidden>
|
||
<div class="ehdr">Add from catalog</div>
|
||
<div id="catalogList"></div>
|
||
<div class="ehdr" style="display:flex;align-items:center;justify-content:space-between;gap:8px">
|
||
<span>Discover more engines</span>
|
||
<input id="engineFilter" type="search" placeholder="Filter by name…" autocomplete="off"
|
||
style="background:#1b2330;color:var(--ink);border:1px solid var(--line);border-radius:6px;padding:4px 8px;font-size:12px;min-width:auto;width:170px">
|
||
</div>
|
||
<div id="extraList"></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. My SearXNG)">
|
||
<input id="engUrl" placeholder="https://example.com/search?q=%s">
|
||
<button id="engAdd" class="btn">Add</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
<!-- PASSWORDS -->
|
||
<section id="passwords" hidden>
|
||
<h1>Passwords</h1>
|
||
<p class="lede">Local password vault. Set once, unlocked with a master password. Encrypted at rest; nothing leaves your machine.</p>
|
||
<!-- State A: no vault yet — set up -->
|
||
<div id="pwSetup" hidden>
|
||
<div class="row" style="flex-direction:column;align-items:stretch;gap:10px">
|
||
<div class="txt"><div class="t">Master password</div>
|
||
<div class="d">Used to unlock the vault every session. This is separate from your Ariadne wallet passphrase — memorize it, we can't recover it.</div></div>
|
||
<div class="addeng"><input id="pwSetupPw1" type="password" placeholder="Master password"><input id="pwSetupPw2" type="password" placeholder="Confirm"></div>
|
||
</div>
|
||
<div class="row" style="flex-direction:column;align-items:stretch;gap:10px">
|
||
<div class="txt"><div class="t">Seed for deterministic passwords</div>
|
||
<div class="d">The "Generate" button in an entry derives a password from this seed. Same seed on another device → same passwords for the same site + username.</div></div>
|
||
<div style="display:flex;flex-direction:column;gap:6px">
|
||
<label class="polrow"><input type="radio" name="pwSeedSource" value="mnemonic" checked><span><b>Use my Ariadne wallet mnemonic</b> <span class="pmuted">— unified identity, one seed to back up</span></span></label>
|
||
<label class="polrow"><input type="radio" name="pwSeedSource" value="generate"><span><b>Generate a new independent seed</b> <span class="pmuted">— isolated from any BCH funds</span></span></label>
|
||
</div>
|
||
<textarea id="pwSetupMnemonic" placeholder="12 or 24 BIP39 words separated by spaces" rows="3" style="background:#1b2330;color:var(--ink);border:1px solid var(--line);border-radius:8px;padding:8px 10px;font-size:13px;font-family:ui-monospace,monospace;outline:none;resize:vertical"></textarea>
|
||
<div class="pmuted" style="font-size:12px">The mnemonic is used only to derive the password-purpose subtree (m/1381'/0'). It is not stored — only the derived subtree key is persisted, encrypted with your master password.</div>
|
||
</div>
|
||
<div class="row" style="justify-content:flex-end">
|
||
<button id="pwSetupBtn" class="btn" type="button">Create vault</button>
|
||
</div>
|
||
</div>
|
||
<!-- State B: vault exists but locked -->
|
||
<div id="pwLocked" hidden>
|
||
<div class="row">
|
||
<div class="txt"><div class="t">Unlock vault</div><div class="d">Enter your master password to view or add entries.</div></div>
|
||
<div class="ctl" style="align-items:stretch"><input id="pwUnlockPw" type="password" placeholder="Master password"><button id="pwUnlockBtn" class="btn" type="button">Unlock</button></div>
|
||
</div>
|
||
<div id="pwUnlockErr" class="pmuted" style="color:#f6768a;font-size:12.5px;margin-top:4px" hidden></div>
|
||
</div>
|
||
<!-- State C: vault unlocked -->
|
||
<div id="pwUnlocked" hidden>
|
||
<div class="row" style="justify-content:space-between">
|
||
<div class="txt"><div class="t">Your passwords</div><div class="d">Reveal, copy, or edit any entry. The vault re-locks when Theseus quits.</div></div>
|
||
<button id="pwLockBtn" class="btn" type="button">Lock now</button>
|
||
</div>
|
||
<div id="pwList"></div>
|
||
<h2 class="sub">Add an entry</h2>
|
||
<div class="row" style="flex-direction:column;align-items:stretch;gap:8px">
|
||
<div class="addeng"><input id="pwAddDomain" placeholder="Site (e.g. github.com)"><input id="pwAddUser" placeholder="Username or email"></div>
|
||
<div style="display:flex;flex-direction:column;gap:6px">
|
||
<label class="polrow"><input type="radio" name="pwAddKind" value="generated" checked><span><b>Generate deterministically</b> <span class="pmuted">— derived from your seed; same across devices</span></span></label>
|
||
<label class="polrow"><input type="radio" name="pwAddKind" value="literal"><span><b>Paste an existing password</b> <span class="pmuted">— for legacy accounts you already set elsewhere</span></span></label>
|
||
</div>
|
||
<input id="pwAddLiteral" type="password" placeholder="Paste password" hidden>
|
||
<div class="addeng"><button id="pwAddPreview" class="btn" type="button" style="flex:none">Preview</button><input id="pwAddPreviewOut" readonly placeholder="preview appears here" style="font-family:ui-monospace,monospace"></div>
|
||
<div style="display:flex;justify-content:flex-end"><button id="pwAddBtn" class="btn" type="button">Save entry</button></div>
|
||
</div>
|
||
<div class="note">There is no autofill yet (phase 2). Copy the password from an entry and paste it into the site.</div>
|
||
</div>
|
||
</section>
|
||
<!-- NAMING -->
|
||
<!-- Registries moved into General (above). -->
|
||
|
||
<!-- 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>
|
||
<!-- Native <select> instead of an <input list=""> datalist — the
|
||
datalist popup was flaky in Electron and never rendered on some
|
||
displays; a real select is unambiguous. -->
|
||
<select id="timezoneValue" hidden>
|
||
<option value="UTC">UTC</option>
|
||
<option value="Europe/London">Europe/London</option>
|
||
<option value="Europe/Berlin">Europe/Berlin</option>
|
||
<option value="Europe/Paris">Europe/Paris</option>
|
||
<option value="Europe/Madrid">Europe/Madrid</option>
|
||
<option value="Europe/Rome">Europe/Rome</option>
|
||
<option value="Europe/Moscow">Europe/Moscow</option>
|
||
<option value="America/New_York">America/New_York</option>
|
||
<option value="America/Chicago">America/Chicago</option>
|
||
<option value="America/Denver">America/Denver</option>
|
||
<option value="America/Los_Angeles">America/Los_Angeles</option>
|
||
<option value="America/Sao_Paulo">America/Sao_Paulo</option>
|
||
<option value="America/Mexico_City">America/Mexico_City</option>
|
||
<option value="America/Toronto">America/Toronto</option>
|
||
<option value="Asia/Tokyo">Asia/Tokyo</option>
|
||
<option value="Asia/Shanghai">Asia/Shanghai</option>
|
||
<option value="Asia/Seoul">Asia/Seoul</option>
|
||
<option value="Asia/Kolkata">Asia/Kolkata</option>
|
||
<option value="Asia/Dubai">Asia/Dubai</option>
|
||
<option value="Asia/Singapore">Asia/Singapore</option>
|
||
<option value="Asia/Bangkok">Asia/Bangkok</option>
|
||
<option value="Australia/Sydney">Australia/Sydney</option>
|
||
<option value="Australia/Perth">Australia/Perth</option>
|
||
<option value="Africa/Nairobi">Africa/Nairobi</option>
|
||
<option value="Africa/Cairo">Africa/Cairo</option>
|
||
<option value="Africa/Johannesburg">Africa/Johannesburg</option>
|
||
<option value="__other__">Other…</option>
|
||
</select>
|
||
<input id="timezoneValueOther" type="text" placeholder="IANA zone, e.g. America/Anchorage" hidden>
|
||
</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>
|
||
<!-- Native <select> (same reason as timezone above). -->
|
||
<select id="languageValue" hidden>
|
||
<option value="en-US">English (US) — en-US</option>
|
||
<option value="en-GB">English (UK) — en-GB</option>
|
||
<option value="zh-CN">Chinese (Simplified) — zh-CN</option>
|
||
<option value="zh-TW">Chinese (Traditional) — zh-TW</option>
|
||
<option value="es-ES">Spanish (Spain) — es-ES</option>
|
||
<option value="es-MX">Spanish (Mexico) — es-MX</option>
|
||
<option value="pt-BR">Portuguese (Brazil) — pt-BR</option>
|
||
<option value="pt-PT">Portuguese (Portugal) — pt-PT</option>
|
||
<option value="fr-FR">French — fr-FR</option>
|
||
<option value="de-DE">German — de-DE</option>
|
||
<option value="it-IT">Italian — it-IT</option>
|
||
<option value="nl-NL">Dutch — nl-NL</option>
|
||
<option value="ru-RU">Russian — ru-RU</option>
|
||
<option value="ja-JP">Japanese — ja-JP</option>
|
||
<option value="ko-KR">Korean — ko-KR</option>
|
||
<option value="hi-IN">Hindi — hi-IN</option>
|
||
<option value="ar">Arabic — ar</option>
|
||
<option value="tr-TR">Turkish — tr-TR</option>
|
||
<option value="pl-PL">Polish — pl-PL</option>
|
||
<option value="uk-UA">Ukrainian — uk-UA</option>
|
||
<option value="sv-SE">Swedish — sv-SE</option>
|
||
<option value="fi-FI">Finnish — fi-FI</option>
|
||
<option value="el-GR">Greek — el-GR</option>
|
||
<option value="he-IL">Hebrew — he-IL</option>
|
||
<option value="vi-VN">Vietnamese — vi-VN</option>
|
||
<option value="th-TH">Thai — th-TH</option>
|
||
<option value="id-ID">Indonesian — id-ID</option>
|
||
<option value="__other__">Other…</option>
|
||
</select>
|
||
<input id="languageValueOther" type="text" placeholder="BCP-47 tag, e.g. cs-CZ" hidden>
|
||
</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>
|
||
<!-- Manual location = pick a city from a native select; its lat/lon
|
||
are looked up client-side (see CITIES below) and written to
|
||
locationLat / locationLon settings, which drive the
|
||
navigator.geolocation override in main.js. The raw lat/lon
|
||
inputs used to sit here but nobody types coordinates by hand;
|
||
the city picker gives the same override with a single click. -->
|
||
<select id="locationCity" hidden>
|
||
<option value="">Pick a city…</option>
|
||
<option value="london">London</option>
|
||
<option value="berlin">Berlin</option>
|
||
<option value="paris">Paris</option>
|
||
<option value="madrid">Madrid</option>
|
||
<option value="rome">Rome</option>
|
||
<option value="moscow">Moscow</option>
|
||
<option value="istanbul">Istanbul</option>
|
||
<option value="dubai">Dubai</option>
|
||
<option value="mumbai">Mumbai</option>
|
||
<option value="singapore">Singapore</option>
|
||
<option value="bangkok">Bangkok</option>
|
||
<option value="shanghai">Shanghai</option>
|
||
<option value="tokyo">Tokyo</option>
|
||
<option value="seoul">Seoul</option>
|
||
<option value="sydney">Sydney</option>
|
||
<option value="new_york">New York</option>
|
||
<option value="los_angeles">Los Angeles</option>
|
||
<option value="chicago">Chicago</option>
|
||
<option value="toronto">Toronto</option>
|
||
<option value="mexico_city">Mexico City</option>
|
||
<option value="sao_paulo">São Paulo</option>
|
||
<option value="buenos_aires">Buenos Aires</option>
|
||
<option value="cairo">Cairo</option>
|
||
<option value="nairobi">Nairobi</option>
|
||
<option value="johannesburg">Johannesburg</option>
|
||
<option value="__other__">Other…</option>
|
||
</select>
|
||
<div id="locationOther" class="coords" hidden>
|
||
<input id="locationLatOther" type="number" step="0.0001" placeholder="lat" style="width:100px">
|
||
<input id="locationLonOther" type="number" step="0.0001" placeholder="lon" style="width:100px">
|
||
</div>
|
||
</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>
|
||
|
||
<h2 class="sub">Storage</h2>
|
||
<p class="subd">By default Theseus keeps <b>nothing</b> across sessions — everything toggled on here is wiped when you quit. Untoggle a bucket to keep it (e.g. cookies to stay signed in on trusted sites).</p>
|
||
<div class="row">
|
||
<div class="txt"><div class="t">Clear cookies on quit</div><div class="d">Drops session + persistent cookies. You'll sign in again next launch.</div></div>
|
||
<label class="sw"><input type="checkbox" id="clearCookiesOnQuit"><span class="track"><span class="knob"></span></span></label>
|
||
</div>
|
||
<div class="row">
|
||
<div class="txt"><div class="t">Clear HTTP cache on quit</div><div class="d">Drops cached images / scripts / stylesheets. Sites re-download; small disk win.</div></div>
|
||
<label class="sw"><input type="checkbox" id="clearCacheOnQuit"><span class="track"><span class="knob"></span></span></label>
|
||
</div>
|
||
<div class="row">
|
||
<div class="txt"><div class="t">Clear site storage on quit</div><div class="d">Drops localStorage, IndexedDB, service workers, and the cache API. Web-app state resets.</div></div>
|
||
<label class="sw"><input type="checkbox" id="clearStorageOnQuit"><span class="track"><span class="knob"></span></span></label>
|
||
</div>
|
||
<div class="row">
|
||
<div class="txt"><div class="t">Clear history on quit</div><div class="d">Drops navigation history + the saved-tabs session file (overrides "Reopen previous tabs").</div></div>
|
||
<label class="sw"><input type="checkbox" id="clearHistoryOnQuit"><span class="track"><span class="knob"></span></span></label>
|
||
</div>
|
||
<div class="row" style="justify-content:flex-end">
|
||
<button id="clearNow" class="btn" type="button">Clear all now</button>
|
||
</div>
|
||
<div class="note">There is no persistent password manager — passwords are never stored to disk regardless of these toggles. Use a dedicated password manager (Bitwarden, KeePass, etc.).</div>
|
||
</section>
|
||
|
||
<!-- ADD-ONS -->
|
||
<section id="addons" hidden>
|
||
<h1>Extensions</h1>
|
||
<p class="lede">Small modules that add capabilities to Theseus. Extensions live as folders under
|
||
<code style="background:transparent;border:none;padding:0" id="addonsPathHint">%APPDATA%\Theseus\extensions\</code>. Drop a folder in, restart, it's live.
|
||
Bundled reference extensions (like the Notepad) are copied there on first run — you can edit or remove them
|
||
without losing anything the browser needs.</p>
|
||
<div class="row" style="justify-content:flex-end;gap:8px">
|
||
<a class="btn" style="text-decoration:none;display:inline-flex;align-items:center" href="https://theseus.x/extensions/" target="_blank" rel="noopener" title="What ships, current signed versions, and how updates are verified">Extensions ↗</a>
|
||
<button id="addonsCheckUpdates" class="btn" type="button">Check for updates</button>
|
||
<button id="addonsReload" class="btn" type="button">Reload</button>
|
||
<button id="addonsOpenDir" class="btn" type="button">Open extensions folder</button>
|
||
</div>
|
||
<div id="addonsUpdStatus" class="pmuted" style="font-size:12.5px;margin-top:6px;text-align:right">—</div>
|
||
<h2 class="sub" style="border-top:0;padding-top:0;margin-top:1.5rem">Installed</h2>
|
||
<div id="addonsList"><div class="d" style="color:var(--dim)">Loading…</div></div>
|
||
<h2 class="sub" style="margin-top:1.5rem">Community</h2>
|
||
<p class="lede" style="margin-top:0">Extensions anyone can publish from <a href="https://theseus.x/extensions/" target="_blank" rel="noopener">theseus.x/extensions</a>
|
||
by signing the package with the wallet that holds a BCDN name. Before installing, Theseus checks that signature
|
||
against the name's current owner in its own chain index — the relay can't substitute code under a trusted name.</p>
|
||
<div class="row" style="justify-content:flex-end;gap:8px"><button id="communityRefresh" class="btn" type="button">Refresh catalog</button></div>
|
||
<div id="communityList"><div class="d" style="color:var(--dim)">Loading…</div></div>
|
||
<div class="note">Extensions run with full app access — treat installing one like installing an unsigned executable.
|
||
Only load extensions whose source you trust.</div>
|
||
</section>
|
||
</div>
|
||
</div>
|
||
<script>
|
||
const C = window.cfg;
|
||
// sidebar navigation
|
||
// "naming" is intentionally absent: the Registries section was folded
|
||
// into General in 0.3.21. Leaving the id in this list threw a
|
||
// TypeError inside showSection (getElementById("naming") → null),
|
||
// which broke every click after General/Search/Passwords.
|
||
const sections = ["general", "search", "passwords", "performance", "privacy", "plugins", "addons"];
|
||
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));
|
||
// Land on the section named by `settings.html#<sec>` when opened via main
|
||
// (e.g. Aegis's "Set up vault" button → open-settings with "passwords").
|
||
try {
|
||
const initSec = String(location.hash || "").replace(/^#/, "").toLowerCase();
|
||
if (initSec) showSection(initSec);
|
||
} catch {}
|
||
|
||
const TOGGLES = ["restoreSession", "backgroundThrottle", "blockCamera", "blockMicrophone", "hideMediaDevices",
|
||
"clearCookiesOnQuit", "clearCacheOnQuit", "clearStorageOnQuit", "clearHistoryOnQuit"];
|
||
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, "<");
|
||
// Try the primary favicon URL (Google's /s2/favicons — set in main.js);
|
||
// fall back to DuckDuckGo's icons.duckduckgo.com service if that 404s,
|
||
// then to the emoji sym as final fallback so the row is never blank.
|
||
const engIcon = (e) => {
|
||
let alt = "";
|
||
try {
|
||
const u = new URL(e.favicon || "https://x");
|
||
const domain = u.searchParams.get("domain") || u.hostname;
|
||
if (domain && domain !== "x") alt = `https://icons.duckduckgo.com/ip3/${domain}.ico`;
|
||
} catch {}
|
||
const sym = (e.sym || "🔍").replace(/'/g, "'");
|
||
const fallbackChain = alt
|
||
? `this.onerror=null;this.src='${alt}';this.setAttribute('data-tried','1');this.onerror=function(){this.replaceWith(Object.assign(document.createElement('span'),{className:'es',textContent:'${sym}'}))};`
|
||
: `this.replaceWith(Object.assign(document.createElement('span'),{className:'es',textContent:'${sym}'}))`;
|
||
return e.favicon
|
||
? `<img class="ei" src="${esc(e.favicon)}" onerror="${fallbackChain}">`
|
||
: `<span class="es">${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 = 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 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="eic">${engIcon(e)}</span><span class="enm">${esc(e.name)}</span>` +
|
||
`<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>` +
|
||
`</div>`;
|
||
list.innerHTML = ENGINE_KINDS.map(({ key, label }) => {
|
||
const rows = installed.filter((e) => (e.kind || "search") === key).map(rowFor).join("");
|
||
if (!rows) return "";
|
||
return `<div class="ehdr">${label}</div>${rows}`;
|
||
}).join("");
|
||
// 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");
|
||
const filterInput = document.getElementById("engineFilter");
|
||
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>`;
|
||
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("")
|
||
: `<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));
|
||
}
|
||
if (extra) {
|
||
const paintExtras = (q) => {
|
||
const filt = String(q || "").trim().toLowerCase();
|
||
const shown = filt ? extraOff.filter((e) => e.name.toLowerCase().includes(filt)) : extraOff;
|
||
extra.innerHTML = shown.length
|
||
? shown.map(catRow).join("")
|
||
: `<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));
|
||
};
|
||
paintExtras(filterInput ? filterInput.value : "");
|
||
if (filterInput && !filterInput.dataset.wired) {
|
||
filterInput.dataset.wired = "1";
|
||
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));
|
||
// 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).
|
||
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);
|
||
});
|
||
});
|
||
}
|
||
// 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);
|
||
// 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);
|
||
// Toolbar sizing dropdowns removed — the toolbar drag-handles do the
|
||
// same job in-place. The stored urlBarSize / searchBoxSize + the drag-
|
||
// set widthPx settings still exist in main; they just have no UI here.
|
||
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"); });
|
||
};
|
||
// Value fields: <select> for tz + lang manual mode, <select> city for
|
||
// location manual mode. Each has an "Other…" sentinel at the end that
|
||
// reveals a text input so the user can enter a value not in the built-in
|
||
// list (any IANA zone, any BCP-47 locale). selectWithOther handles the
|
||
// round-trip: if a saved value isn't in the predefined options, "Other"
|
||
// is auto-selected on load and the input pre-fills with that value.
|
||
function selectWithOther(selectId, otherInputId, settingsKey = selectId) {
|
||
const sel = document.getElementById(selectId);
|
||
const inp = document.getElementById(otherInputId);
|
||
const saved = s[settingsKey] ?? "";
|
||
const known = new Set([...sel.options].map((o) => o.value).filter((v) => v && v !== "__other__"));
|
||
const isCustom = saved && !known.has(saved);
|
||
sel.value = isCustom ? "__other__" : saved;
|
||
if (isCustom) inp.value = saved;
|
||
const applyVis = () => { inp.hidden = sel.value !== "__other__"; };
|
||
applyVis();
|
||
sel.addEventListener("change", () => {
|
||
if (sel.value === "__other__") { applyVis(); setTimeout(() => inp.focus(), 0); return; }
|
||
C.set(settingsKey, sel.value); applyVis();
|
||
});
|
||
inp.addEventListener("change", () => { const v = String(inp.value).trim(); if (v) C.set(settingsKey, v); });
|
||
return sel;
|
||
}
|
||
const tzV = selectWithOther("timezoneValue", "timezoneValueOther");
|
||
const lgV = selectWithOther("languageValue", "languageValueOther");
|
||
const tzOther = document.getElementById("timezoneValueOther");
|
||
const lgOther = document.getElementById("languageValueOther");
|
||
// "Manual" mode reveals both the select AND the Other input (if Other was picked).
|
||
const applyTzVis = (manual) => { tzV.hidden = !manual; tzOther.hidden = !manual || tzV.value !== "__other__"; };
|
||
bind("timezoneMode", null, applyTzVis);
|
||
// language: fixed dropdown when spoofing (top 10), broader dropdown 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";
|
||
lgOther.hidden = lngMode.value !== "manual" || lgV.value !== "__other__";
|
||
};
|
||
lngMode.value = s.languageMode || "show";
|
||
applyLng();
|
||
lngMode.addEventListener("change", () => { C.set("languageMode", lngMode.value); applyLng(); });
|
||
// location: region dropdown when spoofing, city dropdown when manual.
|
||
// Manual writes lat/lon into settings via the CITIES map below; the raw
|
||
// coord inputs were dropped from the UI (nobody types them by hand).
|
||
const locMode = document.getElementById("locationMode");
|
||
const locRegion = document.getElementById("locationRegion");
|
||
const locCity = document.getElementById("locationCity");
|
||
locRegion.value = s.locationRegion || "europe";
|
||
locRegion.addEventListener("change", () => C.set("locationRegion", locRegion.value));
|
||
// Location manual reveals the city select; picking "Other…" reveals a
|
||
// lat/lon pair, whose values are pushed to locationLat/Lon settings.
|
||
const locOther = document.getElementById("locationOther");
|
||
const locLatOther = document.getElementById("locationLatOther");
|
||
const locLonOther = document.getElementById("locationLonOther");
|
||
const applyLoc = () => {
|
||
locRegion.hidden = locMode.value !== "spoof";
|
||
locCity.hidden = locMode.value !== "manual";
|
||
locOther.hidden = locMode.value !== "manual" || locCity.value !== "__other__";
|
||
};
|
||
locMode.value = s.locationMode || "show";
|
||
applyLoc();
|
||
locMode.addEventListener("change", () => { C.set("locationMode", locMode.value); applyLoc(); });
|
||
locCity.addEventListener("change", applyLoc);
|
||
// Prefill Other lat/lon inputs from saved settings.
|
||
locLatOther.value = s.locationLat ?? "";
|
||
locLonOther.value = s.locationLon ?? "";
|
||
const saveOtherCoords = () => {
|
||
const la = String(locLatOther.value).trim();
|
||
const lo = String(locLonOther.value).trim();
|
||
if (la !== "") C.set("locationLat", la);
|
||
if (lo !== "") C.set("locationLon", lo);
|
||
C.set("locationCity", "__other__");
|
||
};
|
||
locLatOther.addEventListener("change", saveOtherCoords);
|
||
locLonOther.addEventListener("change", saveOtherCoords);
|
||
// If the previously-saved city was "__other__", set the select to it so the
|
||
// Other inputs stay visible on reload.
|
||
if (s.locationCity === "__other__") locCity.value = "__other__";
|
||
// Picking a city writes its lat/lon to settings — that's what
|
||
// navigator.geolocation returns to pages once "manual" mode is active.
|
||
const CITIES = {
|
||
london:[51.5074,-0.1278], berlin:[52.52,13.405], paris:[48.8566,2.3522],
|
||
madrid:[40.4168,-3.7038], rome:[41.9028,12.4964], moscow:[55.7558,37.6173],
|
||
istanbul:[41.0082,28.9784], dubai:[25.2048,55.2708], mumbai:[19.076,72.8777],
|
||
singapore:[1.3521,103.8198], bangkok:[13.7563,100.5018], shanghai:[31.2304,121.4737],
|
||
tokyo:[35.6762,139.6503], seoul:[37.5665,126.978], sydney:[-33.8688,151.2093],
|
||
new_york:[40.7128,-74.006], los_angeles:[34.0522,-118.2437], chicago:[41.8781,-87.6298],
|
||
toronto:[43.6532,-79.3832], mexico_city:[19.4326,-99.1332], sao_paulo:[-23.5505,-46.6333],
|
||
buenos_aires:[-34.6037,-58.3816], cairo:[30.0444,31.2357], nairobi:[-1.2921,36.8219],
|
||
johannesburg:[-26.2041,28.0473],
|
||
};
|
||
// Restore prior city selection when possible by matching stored lat/lon.
|
||
const restoreCity = () => {
|
||
const la = Number(s.locationLat), lo = Number(s.locationLon);
|
||
for (const [key, [x, y]] of Object.entries(CITIES))
|
||
if (Math.abs(x - la) < 0.01 && Math.abs(y - lo) < 0.01) { locCity.value = key; return; }
|
||
};
|
||
restoreCity();
|
||
locCity.addEventListener("change", () => {
|
||
const c = CITIES[locCity.value]; if (!c) return;
|
||
C.set("locationLat", String(c[0]));
|
||
C.set("locationLon", String(c[1]));
|
||
C.set("locationCity", locCity.value); // save the pick so we can restore it later
|
||
});
|
||
|
||
// Storage: "Clear all now" wipes everything the toggles cover, without
|
||
// waiting for quit. Confirm first — this signs the user out of everything.
|
||
const clrBtn = document.getElementById("clearNow");
|
||
if (clrBtn) clrBtn.onclick = async () => {
|
||
if (!confirm("Clear cookies, cache, site storage, and history now?\n\nYou'll be signed out of everything and open tabs won't be restored.")) return;
|
||
clrBtn.disabled = true; clrBtn.textContent = "Clearing…";
|
||
try {
|
||
await C.clearBrowsingData({ cookies: true, cache: true, storage: true, history: true });
|
||
clrBtn.textContent = "Cleared ✓";
|
||
} catch (e) { clrBtn.textContent = "Clear failed"; console.error(e); }
|
||
setTimeout(() => { clrBtn.textContent = "Clear all now"; clrBtn.disabled = false; }, 1600);
|
||
};
|
||
|
||
// ---- 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);
|
||
};
|
||
|
||
// ---- Developer tools dock position --------------------------------------
|
||
// The active radio reflects the current setting; changing it just calls
|
||
// C.set — the F12 handler in main.js reads settings.devToolsDock each
|
||
// time DevTools opens, so no restart is needed for the choice to apply.
|
||
const devToolsDock = s.devToolsDock || "bottom";
|
||
document.querySelectorAll('input[name="devToolsDock"]').forEach((r) => {
|
||
r.checked = (r.value === devToolsDock);
|
||
r.addEventListener("change", () => { if (r.checked) C.set("devToolsDock", r.value); });
|
||
});
|
||
|
||
// ---- Ariadne's Thread system-wide resolver toggle -----------------------
|
||
// Ariadne runs as two elevated Scheduled Tasks ("BNS Resolver Daemon" +
|
||
// "BNS Sia Bridge"). Toggling requires admin — main spawns an elevated
|
||
// powershell for start/stop, which prompts UAC once per action.
|
||
const arStatus = document.getElementById("ariadneStatus");
|
||
const arStart = document.getElementById("ariadneStart");
|
||
const arStop = document.getElementById("ariadneStop");
|
||
const arInstall = document.getElementById("ariadneInstall");
|
||
const arUpdate = document.getElementById("ariadneUpdate");
|
||
const arUninstall = document.getElementById("ariadneUninstall");
|
||
const arRefresh = document.getElementById("ariadneRefresh");
|
||
const arMissing = document.getElementById("ariadneMissing");
|
||
async function refreshAriadne() {
|
||
arStatus.textContent = "checking…";
|
||
arStart.hidden = arStop.hidden = arInstall.hidden = arUpdate.hidden = arUninstall.hidden = arMissing.hidden = true;
|
||
try {
|
||
const r = await C.ariadneState();
|
||
const verSuffix = r.installedVersion ? ` (v${r.installedVersion})` : "";
|
||
const bundledSuffix = r.bundledVersion ? ` (bundled v${r.bundledVersion})` : "";
|
||
if (r.state === "running") {
|
||
arStatus.innerHTML = `Status: <b style="color:var(--acid-text)">running</b>${verSuffix} — every browser on this machine resolves BCDN names.`;
|
||
arStop.hidden = false; arUninstall.hidden = !r.hasUninstaller;
|
||
if (r.canUpdate) arUpdate.hidden = false;
|
||
} else if (r.state === "stopped") {
|
||
arStatus.innerHTML = `Status: <b>stopped</b>${verSuffix} — only Theseus resolves BCDN names; other browsers won't.`;
|
||
arStart.hidden = false; arUninstall.hidden = !r.hasUninstaller;
|
||
if (r.canUpdate) arUpdate.hidden = false;
|
||
} else {
|
||
arStatus.innerHTML = `Status: <b>not installed</b> on this machine.${bundledSuffix}`;
|
||
arInstall.hidden = false;
|
||
arMissing.hidden = false;
|
||
}
|
||
} catch (e) { arStatus.textContent = "Status check failed: " + (e?.message || e); }
|
||
}
|
||
// Wrap each async action so the button disables during work + always
|
||
// refreshes state after (success OR failure), so the UI never lies.
|
||
function wire(btn, action, busyLabel) {
|
||
btn.onclick = async () => {
|
||
const orig = btn.textContent;
|
||
btn.disabled = true; btn.textContent = busyLabel;
|
||
try { await action(); } catch {}
|
||
finally { btn.disabled = false; btn.textContent = orig; refreshAriadne(); }
|
||
};
|
||
}
|
||
wire(arStart, () => C.ariadneToggle(true), "Starting…");
|
||
wire(arStop, () => C.ariadneToggle(false), "Stopping…");
|
||
wire(arInstall, () => C.ariadneInstall(), "Installing… (accept the UAC prompt)");
|
||
wire(arUpdate, () => C.ariadneUpdate(), "Updating… (accept the UAC prompt)");
|
||
wire(arUninstall, () => C.ariadneUninstall(), "Uninstalling… (accept the UAC prompt)");
|
||
arRefresh.onclick = refreshAriadne;
|
||
refreshAriadne();
|
||
|
||
// ---- Aegis update card --------------------------------------------------
|
||
// Aegis is a bundled add-on but its updates are shipped OTA via the signed
|
||
// add-on update endpoint, so users can get new wallet versions without
|
||
// updating Theseus. The same addons-check-updates IPC the Extensions page
|
||
// uses is filtered for the aegis id so this card only speaks for Aegis.
|
||
// Legacy id "bchwallet" is also matched for upgrades from pre-rename
|
||
// installs (0.3.x); post-migration the entry lives at id="aegis".
|
||
const aegisStatus = document.getElementById("aegisStatus");
|
||
const aegisCheck = document.getElementById("aegisCheck");
|
||
const aegisApply = document.getElementById("aegisApply");
|
||
const isAegis = (a) => a && (a.id === "aegis" || a.id === "bchwallet");
|
||
// addons-list returns {installed:[...], sidebarPanels:[...]}
|
||
// addons-check-updates returns {report, skipped, staged:[...]}
|
||
// addons-list-staged returns a flat [...]. Normalise both call sites.
|
||
async function refreshAegis() {
|
||
aegisApply.hidden = true;
|
||
try {
|
||
const [listRes, stagedRes] = await Promise.all([
|
||
C.listAddons ? C.listAddons() : Promise.resolve({}),
|
||
C.listStagedAddonUpdates ? C.listStagedAddonUpdates() : Promise.resolve([]),
|
||
]);
|
||
const installed = (listRes && listRes.installed) || [];
|
||
const staged = Array.isArray(stagedRes) ? stagedRes : [];
|
||
const cur = installed.find(isAegis);
|
||
const upd = staged.find(isAegis);
|
||
if (!cur) { aegisStatus.textContent = "Aegis isn't installed. Reinstall Theseus to add it back, or open Settings > Extensions to load it manually."; return; }
|
||
const curVer = cur.version || "?";
|
||
if (upd && upd.version) {
|
||
aegisStatus.innerHTML = `<b style="color:var(--acid-text)">v${upd.version}</b> staged — restart Theseus to switch to it. You're on v${curVer}.`;
|
||
aegisApply.hidden = false;
|
||
} else {
|
||
aegisStatus.textContent = `You're on v${curVer}. Updates arrive over-the-air; the next check runs at boot and every 6h.`;
|
||
}
|
||
} catch (e) { aegisStatus.textContent = "Status check failed: " + (e?.message || e); }
|
||
}
|
||
aegisCheck.onclick = async () => {
|
||
const orig = aegisCheck.textContent;
|
||
aegisCheck.disabled = true; aegisCheck.textContent = "Checking…";
|
||
aegisStatus.textContent = "Checking…";
|
||
try {
|
||
const res = await (C.checkAddonUpdates ? C.checkAddonUpdates() : Promise.resolve({}));
|
||
const staged = (res && Array.isArray(res.staged)) ? res.staged : [];
|
||
const upd = staged.find(isAegis);
|
||
if (!upd) { aegisStatus.textContent = "You're on the latest Aegis."; }
|
||
// refresh will populate everything else + toggle the Apply button
|
||
refreshAegis();
|
||
} catch (e) { aegisStatus.textContent = "Check failed: " + (e?.message || e); }
|
||
finally { aegisCheck.disabled = false; aegisCheck.textContent = orig; }
|
||
};
|
||
aegisApply.onclick = () => { if (C && C.restartApp) C.restartApp(); };
|
||
refreshAegis();
|
||
|
||
// ---- Manual "Check for updates" -----------------------------------------
|
||
// Fires an IPC that un-dismisses any lingering session chip and hits the
|
||
// release manifest. Renders either "you're on the latest (vN.M.O)" or
|
||
// "vX.Y.Z is available — the update chip in the toolbar will offer it".
|
||
const updStatus = document.getElementById("updStatus");
|
||
// Show the current version by default so the user doesn't have to
|
||
// click "Check for updates" just to find out which Theseus they're on.
|
||
if (C && C.appVersion) C.appVersion().then((v) => {
|
||
if (v) updStatus.textContent = `You're on v${v}.`;
|
||
}).catch(() => {});
|
||
const updCheck = document.getElementById("updCheck");
|
||
updCheck.onclick = async () => {
|
||
const orig = updCheck.textContent;
|
||
updCheck.disabled = true; updCheck.textContent = "Checking…";
|
||
updStatus.textContent = "Checking…";
|
||
try {
|
||
const r = await C.recheckUpdate();
|
||
if (r && r.updateAvailable && r.updateAvailable.version) {
|
||
updStatus.innerHTML = `<b style="color:var(--acid-text)">v${r.updateAvailable.version}</b> is available — the update chip in the toolbar will offer it.`;
|
||
} else {
|
||
updStatus.textContent = `You're on the latest (v${(r && r.currentVersion) || "?"}).`;
|
||
}
|
||
} catch (e) { updStatus.textContent = "Check failed: " + (e?.message || e); }
|
||
finally { updCheck.disabled = false; updCheck.textContent = orig; }
|
||
};
|
||
|
||
// ---- Passwords section: three states (setup / locked / unlocked) ---------
|
||
// The vault lives in main.js — this UI just calls IPC. No plaintext ever
|
||
// sits in this DOM except the value produced by a specific Show/Copy click.
|
||
const pwSetupEl = document.getElementById("pwSetup");
|
||
const pwLockedEl = document.getElementById("pwLocked");
|
||
const pwUnlockedEl = document.getElementById("pwUnlocked");
|
||
const pwListEl = document.getElementById("pwList");
|
||
function pwShow(which) {
|
||
pwSetupEl.hidden = which !== "setup";
|
||
pwLockedEl.hidden = which !== "locked";
|
||
pwUnlockedEl.hidden = which !== "unlocked";
|
||
}
|
||
async function pwRefresh() {
|
||
const st = await C.pwStatus();
|
||
if (!st.setup) return pwShow("setup");
|
||
if (!st.unlocked) return pwShow("locked");
|
||
pwShow("unlocked");
|
||
const res = await C.pwList();
|
||
renderPwList(res.ok ? res.entries : []);
|
||
}
|
||
function renderPwList(entries) {
|
||
if (!entries.length) {
|
||
pwListEl.innerHTML = `<div class="cempty" style="padding:12px 0">No entries yet — add one below.</div>`;
|
||
return;
|
||
}
|
||
pwListEl.innerHTML = entries.map((e) => `<div class="eng" data-id="${esc(e.id)}">` +
|
||
`<span class="eic"><img class="ei" src="https://icons.duckduckgo.com/ip3/${esc(e.domain)}.ico" onerror="this.replaceWith(Object.assign(document.createElement('span'),{className:'es',textContent:'🔑'}))"></span>` +
|
||
`<span class="enm"><b>${esc(e.domain)}</b> <span class="pmuted">· ${esc(e.username || "—")}</span> <span class="pmuted" style="font-size:11px">· ${e.kind === "generated" ? "generated" : "pasted"}</span></span>` +
|
||
`<button class="cx pwShow" title="Show + copy">👁</button>` +
|
||
`<button class="cx pwDel" title="Remove">✕</button>` +
|
||
`</div>`).join("");
|
||
pwListEl.querySelectorAll(".pwShow").forEach((b) => b.onclick = async (ev) => {
|
||
const id = ev.target.closest(".eng").dataset.id;
|
||
const res = await C.pwGet(id);
|
||
if (!res.ok) return alert("Couldn't read: " + res.err);
|
||
try { await navigator.clipboard.writeText(res.password); }
|
||
catch { /* browser may block clipboard in dev — fall through to a prompt */ prompt("Password (copy manually):", res.password); return; }
|
||
b.textContent = "copied ✓"; setTimeout(() => (b.textContent = "👁"), 1600);
|
||
});
|
||
pwListEl.querySelectorAll(".pwDel").forEach((b) => b.onclick = async (ev) => {
|
||
const id = ev.target.closest(".eng").dataset.id;
|
||
if (!confirm("Remove this entry?")) return;
|
||
await C.pwRemove(id); pwRefresh();
|
||
});
|
||
}
|
||
// Setup — create vault
|
||
document.querySelectorAll('input[name="pwSeedSource"]').forEach((r) => r.addEventListener("change", () => {
|
||
document.getElementById("pwSetupMnemonic").hidden = document.querySelector('input[name="pwSeedSource"]:checked').value !== "mnemonic";
|
||
}));
|
||
document.getElementById("pwSetupBtn").onclick = async () => {
|
||
const p1 = document.getElementById("pwSetupPw1").value;
|
||
const p2 = document.getElementById("pwSetupPw2").value;
|
||
if (!p1 || p1.length < 8) return alert("Master password must be at least 8 characters.");
|
||
if (p1 !== p2) return alert("Passwords don't match.");
|
||
const kind = document.querySelector('input[name="pwSeedSource"]:checked').value;
|
||
const seedSource = kind === "mnemonic"
|
||
? { kind: "mnemonic", mnemonic: document.getElementById("pwSetupMnemonic").value }
|
||
: { kind: "generate" };
|
||
if (kind === "mnemonic" && !seedSource.mnemonic.trim()) return alert("Paste your mnemonic or switch to 'Generate a new independent seed'.");
|
||
const res = await C.pwSetup(p1, seedSource);
|
||
if (!res.ok) return alert("Setup failed: " + res.err);
|
||
// Vault created AND unlocked by main. Clear the setup fields.
|
||
document.getElementById("pwSetupPw1").value = "";
|
||
document.getElementById("pwSetupPw2").value = "";
|
||
document.getElementById("pwSetupMnemonic").value = "";
|
||
pwRefresh();
|
||
};
|
||
// Unlock
|
||
document.getElementById("pwUnlockBtn").onclick = async () => {
|
||
const err = document.getElementById("pwUnlockErr");
|
||
err.hidden = true;
|
||
const pw = document.getElementById("pwUnlockPw").value;
|
||
const res = await C.pwUnlock(pw);
|
||
if (!res.ok) { err.textContent = res.err; err.hidden = false; return; }
|
||
document.getElementById("pwUnlockPw").value = "";
|
||
pwRefresh();
|
||
};
|
||
document.getElementById("pwUnlockPw").addEventListener("keydown", (e) => { if (e.key === "Enter") document.getElementById("pwUnlockBtn").click(); });
|
||
// Lock
|
||
document.getElementById("pwLockBtn").onclick = async () => { await C.pwLock(); pwRefresh(); };
|
||
// Add-entry form: toggle literal input; wire preview + save
|
||
document.querySelectorAll('input[name="pwAddKind"]').forEach((r) => r.addEventListener("change", () => {
|
||
const kind = document.querySelector('input[name="pwAddKind"]:checked').value;
|
||
document.getElementById("pwAddLiteral").hidden = kind !== "literal";
|
||
document.getElementById("pwAddPreview").hidden = kind !== "generated";
|
||
document.getElementById("pwAddPreviewOut").hidden = kind !== "generated";
|
||
}));
|
||
document.getElementById("pwAddPreview").onclick = async () => {
|
||
const domain = document.getElementById("pwAddDomain").value.trim();
|
||
const username = document.getElementById("pwAddUser").value.trim();
|
||
if (!domain) return alert("Enter a site.");
|
||
const res = await C.pwGenerate({ domain, username });
|
||
if (!res.ok) return alert("Preview failed: " + res.err);
|
||
document.getElementById("pwAddPreviewOut").value = res.password;
|
||
};
|
||
document.getElementById("pwAddBtn").onclick = async () => {
|
||
const domain = document.getElementById("pwAddDomain").value.trim();
|
||
const username = document.getElementById("pwAddUser").value.trim();
|
||
if (!domain) return alert("Enter a site.");
|
||
const kind = document.querySelector('input[name="pwAddKind"]:checked').value;
|
||
const spec = { domain, username };
|
||
if (kind === "literal") {
|
||
const lit = document.getElementById("pwAddLiteral").value;
|
||
if (!lit) return alert("Paste the password to save.");
|
||
spec.literal = lit;
|
||
}
|
||
const res = await C.pwAdd(spec);
|
||
if (!res.ok) return alert("Add failed: " + res.err);
|
||
document.getElementById("pwAddDomain").value = "";
|
||
document.getElementById("pwAddUser").value = "";
|
||
document.getElementById("pwAddLiteral").value = "";
|
||
document.getElementById("pwAddPreviewOut").value = "";
|
||
pwRefresh();
|
||
};
|
||
// Initial state — decide which panel to show now, and every time the user
|
||
// switches to Passwords in the sidebar (so a lock elsewhere is reflected).
|
||
pwRefresh();
|
||
document.querySelector('.side a[data-sec="passwords"]').addEventListener("click", pwRefresh);
|
||
});
|
||
|
||
// ---- Add-ons management ----
|
||
const addonsList = document.getElementById("addonsList");
|
||
// Per-addon update state, keyed by addon id. Populated by loadAddonUpdates()
|
||
// (staged: from listStagedAddonUpdates, background-polled) and by the
|
||
// manual "Check for updates" button (report: fresh per-addon status).
|
||
// renderAddons reads both when drawing each card so the update line lives
|
||
// inside the card — no separate "Pending updates" strip at the top.
|
||
const addonUpdates = { staged: {}, report: {} };
|
||
function updateLineFor(a) {
|
||
const st = addonUpdates.staged[a.id];
|
||
const rep = addonUpdates.report[a.id];
|
||
if (st) {
|
||
return '<div class="d" style="color:var(--acid);margin-top:4px">↻ Update <b>v' + escapeHtml(st.version) + '</b> staged — restart Theseus to apply.</div>';
|
||
}
|
||
if (!rep) return "";
|
||
let msg = "", cls = "color:var(--dim)";
|
||
switch (rep.status) {
|
||
case "up-to-date": msg = "Up to date."; break;
|
||
case "no-update-url": return ""; // don't clutter cards that never opted in
|
||
case "fetch-failed": msg = "Update check failed — " + escapeHtml(rep.detail || "network"); cls = "color:#f6768a"; break;
|
||
case "signature-invalid": msg = "Endpoint offered v" + escapeHtml(rep.newVer || "?") + " with a BAD signature — rejected."; cls = "color:#f6768a"; break;
|
||
case "sha256-mismatch": msg = "Endpoint offered v" + escapeHtml(rep.newVer || "?") + " but the tarball hash didn't match."; cls = "color:#f6768a"; break;
|
||
case "extract-failed": msg = "v" + escapeHtml(rep.newVer || "?") + " downloaded but wouldn't extract — " + escapeHtml(rep.detail || ""); cls = "color:#f6768a"; break;
|
||
case "manifest-mismatch": msg = "Extracted manifest didn't match signed values."; cls = "color:#f6768a"; break;
|
||
case "already-staged": msg = "↻ v" + escapeHtml(rep.newVer || "?") + " already staged — restart to apply."; cls = "color:var(--acid)"; break;
|
||
case "staged": msg = "↻ Staged v" + escapeHtml(rep.newVer || "?") + " — restart to apply."; cls = "color:var(--acid)"; break;
|
||
default: msg = escapeHtml(rep.status || "unknown");
|
||
}
|
||
return '<div class="d" style="' + cls + ';margin-top:4px">' + msg + '</div>';
|
||
}
|
||
function renderAddons(snap) {
|
||
// Plug-in category add-ons (Aegis and future first-class Silent Mode
|
||
// components) live in Settings › Plug-ins with their own product-facing
|
||
// copy — showing them again here as raw extension rows was confusing
|
||
// ("why can I toggle my wallet off from two places?"). Filter them
|
||
// out of the Extensions listing entirely; the Plug-ins tab is the
|
||
// single source of truth for those.
|
||
const items = ((snap && snap.installed) || []).filter((a) => a.category !== "plugin");
|
||
if (!items.length) {
|
||
addonsList.innerHTML = '<div class="d" style="color:var(--dim)">No extensions installed. Drop a folder into the extensions directory to install one.</div>';
|
||
return;
|
||
}
|
||
// Split into built-in (ship with Theseus, reseeded on version bump) vs
|
||
// sideloaded (dropped in by the user). Built-in ones can still be
|
||
// disabled — the toggle just means "load me at startup" — but they
|
||
// can't be permanently deleted since reseedBundledAddons runs on the
|
||
// next launch. Rendering the two groups apart makes that clear.
|
||
const bundled = items.filter((a) => a.bundled);
|
||
const sideloaded = items.filter((a) => !a.bundled);
|
||
const rowFor = (a, isBundled) => {
|
||
if (a.error) {
|
||
return '<div class="row"><div class="txt"><div class="t">⚠ Load failed <span style="color:var(--dim);font-weight:400">' + escapeHtml(a.folder) + '</span></div><div class="d" style="color:#f6768a">' + escapeHtml(a.error) + '</div></div><div><button class="btn" data-reveal="' + escapeAttr(a.folder) + '">Show folder</button></div></div>';
|
||
}
|
||
const caps = (a.capabilities || []).length ? '<span style="color:var(--dim);font-size:11.5px;margin-left:8px">' + a.capabilities.map(escapeHtml).join(", ") + '</span>' : "";
|
||
const builtInBadge = isBundled
|
||
? '<span style="display:inline-block;font-size:10px;letter-spacing:.05em;padding:1px 6px;border-radius:3px;background:rgba(214,255,61,.14);color:var(--acid);font-weight:700;margin-left:6px;vertical-align:middle">BUILT-IN</span>'
|
||
: "";
|
||
// Manifest icons may be an emoji ("🛡"), a "•" fallback, or a
|
||
// data: URI to a branded SVG/PNG — render as <img> for URIs,
|
||
// as escaped text otherwise so the emoji case keeps working.
|
||
const iconHtml = /^data:image\//i.test(a.icon || "")
|
||
? '<img src="' + escapeAttr(a.icon) + '" alt="" style="width:16px;height:16px;vertical-align:middle;margin-right:4px">'
|
||
: escapeHtml(a.icon || "•");
|
||
return '<div class="row"><div class="txt"><div class="t">' + iconHtml + ' ' + escapeHtml(a.name) + builtInBadge + ' <span style="color:var(--dim);font-weight:400">v' + escapeHtml(a.version) + '</span>' + caps + '</div><div class="d">' + escapeHtml(a.description || "") + (a.author ? ' <span style="color:var(--dim)">— ' + escapeHtml(a.author) + '</span>' : '') + '</div>' + updateLineFor(a) + '</div><div style="display:flex;gap:8px;align-items:center"><button class="btn" data-reveal="' + escapeAttr(a.folder) + '">Show folder</button><label class="sw"><input type="checkbox" data-toggle="' + escapeAttr(a.id) + '" ' + (a.enabled ? "checked" : "") + '><span class="track"><span class="knob"></span></span></label></div></div>';
|
||
};
|
||
const sectionHeader = (label, hint) =>
|
||
'<div class="d" style="color:var(--dim);margin:12px 0 6px;font-size:12px;text-transform:uppercase;letter-spacing:.04em">' + escapeHtml(label) + '</div>'
|
||
+ (hint ? '<div class="d" style="color:var(--dim);margin:-4px 0 8px">' + escapeHtml(hint) + '</div>' : '');
|
||
let html = "";
|
||
if (bundled.length) html += sectionHeader("Built into Theseus", "Ship with every Theseus install. Disabling one hides its UI but the folder stays; a fresh install of a newer Theseus reseeds it.")
|
||
+ bundled.map((a) => rowFor(a, true)).join("");
|
||
if (sideloaded.length) html += sectionHeader("Sideloaded", sideloaded.length && bundled.length ? "Third-party or hand-installed. Remove by deleting the folder." : "")
|
||
+ sideloaded.map((a) => rowFor(a, false)).join("");
|
||
addonsList.innerHTML = html;
|
||
addonsList.querySelectorAll('input[data-toggle]').forEach((cb) => {
|
||
cb.addEventListener("change", async () => {
|
||
await C.setAddonEnabled(cb.dataset.toggle, cb.checked);
|
||
loadAddons();
|
||
});
|
||
});
|
||
addonsList.querySelectorAll('button[data-reveal]').forEach((btn) => {
|
||
btn.addEventListener("click", () => C.revealAddon(btn.dataset.reveal));
|
||
});
|
||
}
|
||
// ---- community catalog (theseus.x/extensions) ----
|
||
const communityList = document.getElementById("communityList");
|
||
async function loadCommunity() {
|
||
communityList.innerHTML = '<div class="d" style="color:var(--dim)">Loading…</div>';
|
||
let r;
|
||
try { r = await C.communityCatalog(); } catch (e) { r = { ok: false, error: e?.message || String(e), extensions: [] }; }
|
||
if (!r.ok) { communityList.innerHTML = '<div class="d" style="color:#f6768a">Catalog unavailable: ' + escapeHtml(r.error) + '</div>'; return; }
|
||
if (!r.extensions.length) { communityList.innerHTML = '<div class="d" style="color:var(--dim)">Nothing published yet. Be the first — see theseus.x/extensions.</div>'; return; }
|
||
communityList.innerHTML = r.extensions.map((e) => {
|
||
const state = e.installedVersion
|
||
? (e.canUpdate ? '<button class="btn" data-install="' + escapeAttr(e.id) + '">Update to ' + escapeHtml(e.latest) + '</button>'
|
||
: '<span style="color:var(--acid);font-size:12.5px">Installed v' + escapeHtml(e.installedVersion) + '</span>')
|
||
: '<button class="btn" data-install="' + escapeAttr(e.id) + '">Install</button>';
|
||
return '<div class="row"><div class="txt"><div class="t">' + escapeHtml(e.icon || "🧩") + ' ' + escapeHtml(e.name)
|
||
+ ' <span style="color:var(--dim);font-weight:400">v' + escapeHtml(e.latest) + '</span></div>'
|
||
+ '<div class="d">' + escapeHtml(e.description || "") + '</div>'
|
||
+ '<div class="d" style="color:var(--dim);font-size:12px">Published by <b>' + escapeHtml(e.publisher) + '</b> · ' + escapeHtml(String(e.versions || 1)) + ' version' + (e.versions === 1 ? "" : "s")
|
||
+ (e.capabilities && e.capabilities.length ? ' · ' + escapeHtml(e.capabilities.join(", ")) : "") + '</div>'
|
||
+ '<div class="d" data-status="' + escapeAttr(e.id) + '"></div></div><div>' + state + '</div></div>';
|
||
}).join("");
|
||
communityList.querySelectorAll("button[data-install]").forEach((btn) => {
|
||
btn.addEventListener("click", async () => {
|
||
const id = btn.dataset.install;
|
||
const status = communityList.querySelector('[data-status="' + CSS.escape(id) + '"]');
|
||
btn.disabled = true; const orig = btn.textContent; btn.textContent = "Verifying…";
|
||
let res;
|
||
try { res = await C.installCommunity(id); } catch (e) { res = { ok: false, error: e?.message || String(e) }; }
|
||
if (res && res.ok) { status.style.color = "var(--acid)"; status.textContent = "✓ Installed v" + res.version + " (signed by " + res.publisher + ")"; loadAddons(); loadCommunity(); }
|
||
else { status.style.color = "#f6768a"; status.textContent = "✗ " + (res?.error || "install failed"); btn.disabled = false; btn.textContent = orig; }
|
||
});
|
||
});
|
||
}
|
||
document.getElementById("communityRefresh").addEventListener("click", loadCommunity);
|
||
function escapeHtml(s) { return String(s || "").replace(/[&<>"']/g, (c) => ({ "&":"&","<":"<",">":">",'"':""","'":"'" })[c]); }
|
||
function escapeAttr(s) { return escapeHtml(s); }
|
||
async function loadAddons() {
|
||
try { renderAddons(await C.listAddons()); }
|
||
catch (e) { addonsList.textContent = "Failed to load extensions: " + (e?.message || e); }
|
||
}
|
||
// Refresh the staged-updates map from the background poll. Whatever came
|
||
// back through listStagedAddonUpdates is what promoteStagedUpdates will
|
||
// pick up on the next launch — the per-card badge reflects exactly that.
|
||
async function loadAddonUpdates() {
|
||
let staged = [];
|
||
try { staged = await C.listStagedAddonUpdates(); } catch { staged = []; }
|
||
addonUpdates.staged = {};
|
||
for (const s of (staged || [])) addonUpdates.staged[s.id] = { version: s.version, name: s.name };
|
||
}
|
||
document.getElementById("addonsReload").addEventListener("click", async () => {
|
||
await C.reloadAddons(); await loadAddonUpdates(); loadAddons();
|
||
});
|
||
document.getElementById("addonsOpenDir").addEventListener("click", () => C.openAddonsDir());
|
||
document.querySelector('.side a[data-sec="addons"]').addEventListener("click", async () => {
|
||
await loadAddonUpdates(); loadAddons(); loadCommunity();
|
||
});
|
||
// Plug-ins tab: refresh Ariadne's live daemon state on every visit so it
|
||
// doesn't display stale "checking…" text if the background poll finished
|
||
// while another section was open. Aegis card loads on page-init and its
|
||
// "Check for updates" button is user-initiated, so we don't auto-poll it
|
||
// (that would fire a network request every tab-switch).
|
||
document.querySelector('.side a[data-sec="plugins"]').addEventListener("click", () => {
|
||
const ar = document.getElementById("ariadneRefresh");
|
||
if (ar) ar.click();
|
||
});
|
||
|
||
// (Old separate "Pending updates" strip lived here. Per-card update
|
||
// line is now painted by updateLineFor() inside each addon row —
|
||
// loadAddonUpdates() populates addonUpdates.staged before renderAddons.)
|
||
document.getElementById("addonsCheckUpdates").addEventListener("click", async () => {
|
||
const btn = document.getElementById("addonsCheckUpdates");
|
||
const status = document.getElementById("addonsUpdStatus");
|
||
btn.disabled = true; const orig = btn.textContent; btn.textContent = "Checking…";
|
||
status.innerHTML = "";
|
||
try {
|
||
const res = await C.checkAddonUpdates();
|
||
// Back-compat: some callers still pass a bare array. Normalize.
|
||
const report = Array.isArray(res) ? [] : (res?.report || []);
|
||
const skipped = Array.isArray(res) ? null : (res?.skipped || null);
|
||
const staged = Array.isArray(res) ? res : (res?.staged || []);
|
||
if (skipped === "no-pubkeys") {
|
||
status.textContent = "Update endpoint disabled — no operator pubkey baked into this build.";
|
||
} else {
|
||
// Fold the per-addon report into addonUpdates.report so the
|
||
// per-card update line reflects the freshest check. A summary at
|
||
// the top counts staged/updated vs. up-to-date, but the per-addon
|
||
// detail lives on each card.
|
||
addonUpdates.report = {};
|
||
for (const r of report) addonUpdates.report[r.id] = r;
|
||
const stagedNow = report.filter((r) => r.status === "staged" || r.status === "already-staged").length;
|
||
const failed = report.filter((r) => ["fetch-failed","signature-invalid","sha256-mismatch","extract-failed","manifest-mismatch"].includes(r.status)).length;
|
||
if (stagedNow) {
|
||
status.textContent = stagedNow + " update" + (stagedNow > 1 ? "s" : "") + " staged; restart Theseus to apply.";
|
||
} else if (failed) {
|
||
status.textContent = failed + " failed — see the extension card" + (failed > 1 ? "s" : "") + " below.";
|
||
} else if (!report.length) {
|
||
status.textContent = "No extensions with an update endpoint.";
|
||
} else {
|
||
status.textContent = "All extensions up to date.";
|
||
}
|
||
}
|
||
await loadAddonUpdates();
|
||
loadAddons();
|
||
} catch (e) {
|
||
status.textContent = "Check failed: " + (e?.message || e);
|
||
} finally {
|
||
btn.disabled = false; btn.textContent = orig;
|
||
}
|
||
});
|
||
// Populate on first paint so the tab is ready when the user clicks in.
|
||
(async () => { await loadAddonUpdates(); loadAddons(); loadCommunity(); })();
|
||
</script>
|
||
</body>
|
||
</html>
|