feat(theseus): Settings pages have addresses and sub-pages; Privacy regrouped
Navigation base, the way Firefox does about:preferences#privacy: the address bar follows the Settings page (theseus://settings/privacy) and the hash mirrors it, so every page has a link; a page can have sub-pages (theseus://settings/privacy/exceptions) with a breadcrumb and a back arrow; open-settings and theseus:// links accept the two-level slug. Privacy now reads top-down: a "Theseus is on guard" card (Shield and its running total, cookie pop-ups answered, Tor state, version), then Tracking protection with the Shield and Cookie Pop-ups cards moved here from Performance and a Manage exceptions sub-page listing the sites each add-on was told to leave alone (remove to protect again), then Device access, Anti-fingerprinting, Network (Tor switch and the VPN panel) and Browsing data. Performance is about resources again.
This commit is contained in:
parent
305bda7c3e
commit
f1b1de5a9e
5 changed files with 138 additions and 26 deletions
|
|
@ -193,7 +193,8 @@ async function activate(a) {
|
|||
persistAllow(); pushState(); return snapshot();
|
||||
});
|
||||
api.onMessage("refresh-lists", () => refreshLists("manual"));
|
||||
api.onMessage("open-settings", () => { api.openSettings("performance"); return true; });
|
||||
api.onMessage("open-settings", () => { api.openSettings("privacy"); return true; });
|
||||
api.onMessage("list-exceptions", () => [...allow].sort());
|
||||
|
||||
// Filter first, engine second: requests made while the engine builds pass
|
||||
// through (a few hundred milliseconds on first run), which beats holding
|
||||
|
|
|
|||
|
|
@ -88,7 +88,8 @@ async function activate(a) {
|
|||
});
|
||||
// From the panel.
|
||||
api.onMessage("state", () => snapshot());
|
||||
api.onMessage("open-settings", () => { api.openSettings("performance"); return true; });
|
||||
api.onMessage("open-settings", () => { api.openSettings("privacy"); return true; });
|
||||
api.onMessage("list-exceptions", () => [...skip].sort());
|
||||
api.onMessage("set-enabled", (p) => { enabled = !!(p && p.enabled); api.storage.set("enabled", enabled); pushState(); return snapshot(); });
|
||||
api.onMessage("set-mode", (p) => { mode = p && p.mode === "optIn" ? "optIn" : "optOut"; api.storage.set("mode", mode); pushState(); return snapshot(); });
|
||||
api.onMessage("skip-site", (p) => {
|
||||
|
|
|
|||
17
main.js
17
main.js
|
|
@ -2134,7 +2134,7 @@ function initAddons() {
|
|||
// so the same section-hint validation applies. Add-ons hit this when they
|
||||
// want to point users at Passwords, Extensions, etc.
|
||||
openSettings: (section) => {
|
||||
const slug = typeof section === "string" && /^[a-z0-9-]{1,32}$/i.test(section) ? section.toLowerCase() : "";
|
||||
const slug = typeof section === "string" && /^[a-z0-9-]{1,32}(?:\/[a-z0-9-]{1,32})?$/i.test(section) ? section.toLowerCase() : "";
|
||||
const ex = tabs.find((t) => t.settings);
|
||||
if (ex) {
|
||||
setActive(ex.id);
|
||||
|
|
@ -3570,7 +3570,7 @@ async function navigateTab(id, input) {
|
|||
if (installId) { installExtensionWithConsent(installId, null); return; }
|
||||
// theseus://settings, theseus://settings/<section>: a linkable address for
|
||||
// every Settings page.
|
||||
const settingsLink = /^theseus:\/\/settings(?:\/([a-z0-9-]{1,32}))?\/?$/i.exec(q);
|
||||
const settingsLink = /^theseus:\/\/settings(?:\/([a-z0-9-]{1,32}(?:\/[a-z0-9-]{1,32})?))?\/?$/i.exec(q);
|
||||
if (settingsLink) { openSettingsTab(settingsLink[1] || ""); return; }
|
||||
// Local paths open as files — never BCNR, never a search.
|
||||
const fileUrl = localFileUrl(q);
|
||||
|
|
@ -4912,7 +4912,7 @@ ipcMain.handle("toggle-tor", () => { torState === "off" ? startTor() : stopTor()
|
|||
// settings.html watches for a fragment on load and an IPC message when
|
||||
// already loaded.
|
||||
function openSettingsTab(section) {
|
||||
const slug = typeof section === "string" && /^[a-z0-9-]{1,32}$/i.test(section) ? section.toLowerCase() : "";
|
||||
const slug = typeof section === "string" && /^[a-z0-9-]{1,32}(?:\/[a-z0-9-]{1,32})?$/i.test(section) ? section.toLowerCase() : "";
|
||||
const ex = tabs.find((t) => t.settings);
|
||||
if (ex) {
|
||||
setActive(ex.id);
|
||||
|
|
@ -4930,6 +4930,17 @@ ipcMain.handle("addon-invoke", (e, id, msg, payload) => {
|
|||
if (!addonHost) throw new Error("no add-on host");
|
||||
return addonHost.dispatch(String(id || ""), String(msg || ""), payload, { from: "settings" });
|
||||
});
|
||||
// The Settings page reports which page it shows; the address bar follows
|
||||
// (theseus://settings/<slug>), so every Settings page has a link.
|
||||
ipcMain.handle("settings-section", (e, slug) => {
|
||||
const t = tabs.find((x) => x.settings && x.view?.webContents === e.sender);
|
||||
if (!t) return false;
|
||||
const s = typeof slug === "string" && /^[a-z0-9-]{1,32}(?:\/[a-z0-9-]{1,32})?$/i.test(slug) ? slug.toLowerCase() : "";
|
||||
t.url = s ? `theseus://settings/${s}` : "theseus://settings";
|
||||
emitTabs();
|
||||
return true;
|
||||
});
|
||||
ipcMain.handle("tor-state", (e) => { if (!isSettingsSender(e)) throw new Error("tor-state: settings only"); return torState; });
|
||||
ipcMain.handle("settings-open-panel", (e, panelId) => {
|
||||
if (!isSettingsSender(e)) throw new Error("settings-open-panel: settings only");
|
||||
setSidebar(true, String(panelId || ""));
|
||||
|
|
|
|||
|
|
@ -53,6 +53,10 @@ contextBridge.exposeInMainWorld("cfg", {
|
|||
// handlers (Shield, Cookie Pop-ups) and open its panel for the details.
|
||||
addonInvoke: (id, msg, payload) => ipcRenderer.invoke("addon-invoke", String(id || ""), String(msg || ""), payload),
|
||||
openPanel: (panelId) => ipcRenderer.invoke("settings-open-panel", String(panelId || "")),
|
||||
// Which page is showing (the address bar follows), Tor state + toggle for Privacy › Network.
|
||||
reportSection: (slug) => ipcRenderer.invoke("settings-section", String(slug || "")),
|
||||
torState: () => ipcRenderer.invoke("tor-state"),
|
||||
toggleTor: () => ipcRenderer.invoke("toggle-tor"),
|
||||
openAddonsDir: () => ipcRenderer.invoke("addons-open-dir"),
|
||||
reloadAddons: () => ipcRenderer.invoke("addons-reload"),
|
||||
// Community extensions from theseus.x/extensions: the catalog (with what
|
||||
|
|
|
|||
137
settings.html
137
settings.html
|
|
@ -34,7 +34,17 @@
|
|||
.row .acts{display:flex;gap:8px;align-items:center;margin-top:10px;flex-wrap:wrap}
|
||||
.btn.small{padding:5px 10px;font-size:12px}
|
||||
.btn.ghost{background:transparent;border-color:var(--line)}
|
||||
h2.sub{font-size:12px;font-weight:600;letter-spacing:.06em;text-transform:uppercase;color:var(--mut);margin:22px 0 8px}
|
||||
.crumb{font-size:13px;color:var(--mut);margin:0 0 .6rem}.crumb a{color:var(--acid-text);cursor:pointer;text-decoration:none}.crumb a:hover{text-decoration:underline}
|
||||
h1 .back{border:none;background:transparent;color:var(--mut);font:inherit;font-size:1.3rem;line-height:1;cursor:pointer;padding:0 8px 0 0;vertical-align:-1px}h1 .back:hover{color:var(--ink)}
|
||||
.status{background:var(--panel);border:1px solid var(--line);border-radius:12px;padding:16px 18px;margin-bottom:10px}
|
||||
.status .h{font-weight:600;margin-bottom:8px}
|
||||
.srow{display:flex;gap:10px;align-items:center;font-size:13.5px;color:var(--ink);padding:3px 0}
|
||||
.srow::before{content:"";width:9px;height:9px;border-radius:50%;background:var(--dim);flex:none}
|
||||
.srow.ok::before{background:#3ddc84}.srow.warn::before{background:#f5a524}
|
||||
.row.link{cursor:pointer}.row.link:hover{border-color:rgb(from var(--acid) r g b / .4)}.row .chev{color:var(--dim);font-size:20px;line-height:1}
|
||||
.exlist{display:flex;flex-direction:column;gap:6px;margin-bottom:14px}
|
||||
.exrow{display:flex;align-items:center;justify-content:space-between;gap:12px;background:var(--panel);border:1px solid var(--line);border-radius:10px;padding:9px 14px;font:13px ui-monospace,"Cascadia Code",Consolas,monospace}
|
||||
.exempty{color:var(--dim);font-size:13px;margin:0 0 14px}
|
||||
/* 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%}
|
||||
|
|
@ -425,12 +435,24 @@
|
|||
<!-- PERFORMANCE -->
|
||||
<section id="performance" hidden>
|
||||
<h1>Performance</h1>
|
||||
<p class="lede">Keep Theseus light on resources, and keep trackers, ads and cookie banners out of your way.</p>
|
||||
<p class="lede">Keep Theseus light on resources. Trackers, ads and cookie pop-ups are under <a data-go="privacy" style="color:var(--acid-text);cursor:pointer">Privacy</a>.</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>
|
||||
<h2 class="sub">Protections</h2>
|
||||
</section>
|
||||
<!-- PRIVACY -->
|
||||
<section id="privacy" hidden>
|
||||
<h1>Privacy</h1>
|
||||
<p class="lede">What Theseus keeps from sites: trackers, ads, cookie pop-ups, your device and your whereabouts.</p>
|
||||
<div class="status" id="guard">
|
||||
<div class="h">Theseus is on guard</div>
|
||||
<div class="srow" id="stShieldRow"><span id="stShield">Shield…</span></div>
|
||||
<div class="srow" id="stConsentRow"><span id="stConsent">Cookie pop-ups…</span></div>
|
||||
<div class="srow" id="stTorRow"><span id="stTor">Tor…</span></div>
|
||||
<div class="srow ok"><span id="stVersion">Theseus</span></div>
|
||||
</div>
|
||||
<h2 class="sub">Tracking protection</h2>
|
||||
<div class="row" id="shieldCard" hidden>
|
||||
<div class="txt"><div class="t">Shield — block trackers and ads</div>
|
||||
<div class="d">Requests to known tracking and advertising hosts never leave Theseus. <span id="shieldStats"></span></div>
|
||||
|
|
@ -443,12 +465,12 @@
|
|||
<div class="acts"><select id="consentMode"><option value="optOut">Reject all but essentials</option><option value="optIn">Just make it go away</option></select><button class="btn small ghost" id="consentPanel">Open panel</button></div></div>
|
||||
<label class="sw"><input type="checkbox" id="consentOn"><span class="track"><span class="knob"></span></span></label>
|
||||
</div>
|
||||
<div class="row link" data-go="privacy/exceptions">
|
||||
<div class="txt"><div class="t">Manage exceptions</div><div class="d" id="exSummary">Sites where Shield is allowed through or cookie pop-ups are left alone.</div></div>
|
||||
<span class="chev">›</span>
|
||||
</div>
|
||||
<p class="note">Both also sit in the toolbar's extension row if you want them there: right-click the row and choose "Show hidden".</p>
|
||||
</section>
|
||||
<!-- PRIVACY -->
|
||||
<section id="privacy" hidden>
|
||||
<h1>Privacy</h1>
|
||||
<p class="lede">Reduce tracking and lock down device access.</p>
|
||||
<h2 class="sub">Device access</h2>
|
||||
<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>
|
||||
|
|
@ -619,7 +641,16 @@
|
|||
</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>
|
||||
<h2 class="sub">Network</h2>
|
||||
<div class="row">
|
||||
<div class="txt"><div class="t">Tor onion routing</div><div class="d" id="torStat">Off</div></div>
|
||||
<label class="sw"><input type="checkbox" id="torOn"><span class="track"><span class="knob"></span></span></label>
|
||||
</div>
|
||||
<div class="row link" id="vpnRow">
|
||||
<div class="txt"><div class="t">VPN</div><div class="d">Route everything through a Silent Mode exit, or your own. Managed in its panel.</div></div>
|
||||
<span class="chev">›</span>
|
||||
</div>
|
||||
<h2 class="sub">Browsing data</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>
|
||||
|
|
@ -644,6 +675,16 @@
|
|||
</section>
|
||||
|
||||
<!-- ADD-ONS -->
|
||||
<!-- PRIVACY › EXCEPTIONS (sub-page) -->
|
||||
<section id="privacy-exceptions" class="subpage" data-parent="privacy" hidden>
|
||||
<div class="crumb"><a data-go="privacy">Privacy</a> › Exceptions</div>
|
||||
<h1><button class="back" data-go="privacy" title="Back to Privacy" aria-label="Back">‹</button>Exceptions</h1>
|
||||
<p class="lede">Sites you asked Shield or Cookie Pop-ups to leave alone. Remove one to protect it again.</p>
|
||||
<h2 class="sub">Shield allowed through</h2>
|
||||
<div class="exlist" id="shieldEx"></div>
|
||||
<h2 class="sub">Cookie pop-ups left alone</h2>
|
||||
<div class="exlist" id="consentEx"></div>
|
||||
</section>
|
||||
<section id="addons" hidden>
|
||||
<h1>Extensions</h1>
|
||||
<p class="lede">Small modules that add capabilities to Theseus. Extensions live as folders under
|
||||
|
|
@ -678,21 +719,34 @@
|
|||
// 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;
|
||||
// Pages are <section id="..."> elements; a sub-page is <section id="parent-sub"
|
||||
// class="subpage" data-parent="parent"> and is addressed as "parent/sub". The
|
||||
// address bar follows (theseus://settings/<slug>) and the hash mirrors it, so
|
||||
// every page has a link.
|
||||
const sections = [...document.querySelectorAll("section[id]")].map((x) => x.id);
|
||||
let currentSlug = "general";
|
||||
function showSection(slug) {
|
||||
slug = String(slug || "general").toLowerCase().replace(/^#/, "").replace(/^\/+|\/+$/g, "");
|
||||
const [sec, sub] = slug.split("/");
|
||||
const id = sub ? `${sec}-${sub}` : sec;
|
||||
if (!sections.includes(id)) return false;
|
||||
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);
|
||||
for (const s of sections) document.getElementById(s).hidden = (s !== id);
|
||||
try { document.querySelector(".content").scrollTop = 0; } catch {}
|
||||
currentSlug = sub ? `${sec}/${sub}` : sec;
|
||||
try { history.replaceState(null, "", "#" + currentSlug); } catch {}
|
||||
try { C && C.reportSection && C.reportSection(currentSlug); } catch {}
|
||||
document.dispatchEvent(new CustomEvent("section", { detail: currentSlug }));
|
||||
return true;
|
||||
}
|
||||
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).
|
||||
document.addEventListener("click", (e) => { const g = e.target.closest("[data-go]"); if (g) { e.preventDefault(); showSection(g.dataset.go); } });
|
||||
// Main-process asks us to jump to a page (open-settings, theseus://settings/… links).
|
||||
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").
|
||||
// Land on the page named by `settings.html#<slug>` when opened via main.
|
||||
try {
|
||||
const initSec = String(location.hash || "").replace(/^#/, "").toLowerCase();
|
||||
if (initSec) showSection(initSec);
|
||||
showSection(initSec || "general");
|
||||
} catch {}
|
||||
|
||||
// Protections (Performance): Shield + Cookie Pop-ups, driven through the
|
||||
|
|
@ -721,9 +775,50 @@
|
|||
el("consentOn").addEventListener("change", () => inv("consent", "set-enabled", { enabled: el("consentOn").checked }).then(refresh, refresh));
|
||||
el("consentMode").addEventListener("change", () => inv("consent", "set-mode", { mode: el("consentMode").value }).then(refresh, refresh));
|
||||
el("consentPanel").addEventListener("click", () => C.openPanel && C.openPanel("consent:main"));
|
||||
document.querySelector('.side a[data-sec="performance"]').addEventListener("click", refresh);
|
||||
if (C.onFocusSection) C.onFocusSection((sec) => { if (sec === "performance") refresh(); });
|
||||
refresh();
|
||||
// Status card, Network rows and the exceptions sub-page.
|
||||
function renderList(id, hosts, onRemove) {
|
||||
const box = el(id); box.innerHTML = "";
|
||||
if (!hosts || !hosts.length) { box.innerHTML = '<p class="exempty">No sites yet.</p>'; return; }
|
||||
for (const h of hosts) {
|
||||
const row = document.createElement("div"); row.className = "exrow";
|
||||
const name = document.createElement("span"); name.textContent = h;
|
||||
const btn = document.createElement("button"); btn.className = "btn small ghost"; btn.type = "button"; btn.textContent = "Remove";
|
||||
btn.onclick = () => { btn.disabled = true; onRemove(h); };
|
||||
row.append(name, btn); box.appendChild(row);
|
||||
}
|
||||
}
|
||||
async function refreshExtra() {
|
||||
let shieldEx = [], consentEx = [];
|
||||
try { shieldEx = (await inv("blocker", "list-exceptions")) || []; } catch {}
|
||||
try { consentEx = (await inv("consent", "list-exceptions")) || []; } catch {}
|
||||
renderList("shieldEx", shieldEx, (h) => inv("blocker", "allow-site", { host: h, allowed: false }).then(refreshAll, refreshAll));
|
||||
renderList("consentEx", consentEx, (h) => inv("consent", "skip-site", { host: h, skipped: false }).then(refreshAll, refreshAll));
|
||||
const n = shieldEx.length + consentEx.length;
|
||||
el("exSummary").textContent = n ? `${n} site${n === 1 ? "" : "s"}: ${[...shieldEx, ...consentEx].slice(0, 3).join(", ")}${n > 3 ? "…" : ""}` : "Sites where Shield is allowed through or cookie pop-ups are left alone.";
|
||||
try {
|
||||
const s = await inv("blocker", "state");
|
||||
el("stShield").textContent = s.enabled ? `Shield is on — ${Number(s.blockedTotal || 0).toLocaleString()} requests blocked so far` : "Shield is off";
|
||||
el("stShieldRow").className = "srow " + (s.enabled ? "ok" : "warn");
|
||||
} catch { el("stShield").textContent = "Shield is not installed"; el("stShieldRow").className = "srow warn"; }
|
||||
try {
|
||||
const s = await inv("consent", "state");
|
||||
el("stConsent").textContent = s.enabled ? `Cookie pop-ups are answered for you — ${Number(s.handledTotal || 0).toLocaleString()} so far` : "Cookie pop-ups are left to you";
|
||||
el("stConsentRow").className = "srow " + (s.enabled ? "ok" : "warn");
|
||||
} catch { el("stConsent").textContent = "Cookie Pop-ups is not installed"; el("stConsentRow").className = "srow warn"; }
|
||||
try {
|
||||
const t = C.torState ? await C.torState() : "off";
|
||||
el("torOn").checked = t !== "off";
|
||||
el("torStat").textContent = t === "on" ? "On — page traffic leaves through the Tor network." : t === "connecting" ? "Connecting…" : "Off — traffic goes out directly (or through the VPN when it is on).";
|
||||
el("stTor").textContent = t === "on" ? "Tor is on" : t === "connecting" ? "Tor is connecting" : "Tor is off — turn it on below when you need it";
|
||||
el("stTorRow").className = "srow " + (t === "on" ? "ok" : "");
|
||||
} catch {}
|
||||
try { const v = C.appVersion ? await C.appVersion() : ""; el("stVersion").textContent = v ? `Theseus ${v}` : "Theseus"; } catch {}
|
||||
}
|
||||
const refreshAll = () => refresh().then(refreshExtra, refreshExtra);
|
||||
el("torOn").addEventListener("change", () => { if (C.toggleTor) C.toggleTor().catch(() => {}); setTimeout(refreshExtra, 1200); setTimeout(refreshExtra, 4000); });
|
||||
el("vpnRow").addEventListener("click", () => C.openPanel && C.openPanel("vpn:main"));
|
||||
document.addEventListener("section", (e) => { if (String(e.detail).startsWith("privacy")) refreshAll(); });
|
||||
refreshAll();
|
||||
})();
|
||||
const TOGGLES = ["restoreSession", "backgroundThrottle", "blockCamera", "blockMicrophone", "hideMediaDevices",
|
||||
"clearCookiesOnQuit", "clearCacheOnQuit", "clearStorageOnQuit", "clearHistoryOnQuit"];
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue