feat(theseus): protections live in Settings › Performance; quiet dock for Shield and Cookie Pop-ups
Shield and Cookie Pop-ups are settings more than tools, so their switches, the cookie mode, the counters and "Update rules" now sit in Settings › Performance under a Protections heading, driven through the add-ons' own message handlers (Settings-only IPC). Each card opens the add-on's panel for the per-site details, and each panel links back to Settings. The two add-ons start hidden from the toolbar's extension row (manifest dock:"hidden", honoured once so a user who shows them keeps them); "Show hidden" on the row brings them back. theseus://settings and theseus://settings/<section> are now addresses, so any page or note can link to a Settings page. Also: a Settings or add-on tab that the user navigates elsewhere stops counting as that tab, otherwise "open Settings" kept focusing a tab that no longer showed Settings.
This commit is contained in:
parent
70934a7553
commit
2230d4200c
10 changed files with 110 additions and 4 deletions
|
|
@ -208,7 +208,10 @@ function validateManifest(raw, folderName) {
|
|||
// back to plain-extension rendering.
|
||||
const category = m.category && ["plugin"].includes(String(m.category))
|
||||
? String(m.category) : null;
|
||||
return { id, name, version, description, author, icon, main, capabilities, pageInject, toolbarMenu, contextMenuItems, absorbs, category };
|
||||
// dock: "hidden" — the add-on starts without a toolbar button (its settings
|
||||
// live in Settings); main honours it once, the user can show it later.
|
||||
const dock = m.dock === "hidden" ? "hidden" : undefined;
|
||||
return { id, name, version, description, author, icon, main, capabilities, pageInject, toolbarMenu, contextMenuItems, absorbs, category, dock };
|
||||
}
|
||||
|
||||
// Loader singleton. `discoverAndActivate(opts)` returns a snapshot the rest
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
"author": "Silent Mode",
|
||||
"icon": "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCI+PHBhdGggZD0iTTEyIDJMNCA1djdjMCA1IDMuNSA5LjIgOCAxMCA0LjUtLjggOC01IDgtMTBWNWwtOC0zeiIgZmlsbD0iIzRjOWVmZiIvPjxwYXRoIGQ9Ik05LjIgOS4yIDE0LjggMTQuOE0xNC44IDkuMiA5LjIgMTQuOCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjZmZmIiBzdHJva2Utd2lkdGg9IjIiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIvPjwvc3ZnPg==",
|
||||
"main": "index.js",
|
||||
"dock": "hidden",
|
||||
"capabilities": ["sidebar-panel", "request-filter"],
|
||||
"updateURL": "https://navigate.st/bns/theseus.x/extensions/blocker/updates.json"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -193,6 +193,7 @@ async function activate(a) {
|
|||
persistAllow(); pushState(); return snapshot();
|
||||
});
|
||||
api.onMessage("refresh-lists", () => refreshLists("manual"));
|
||||
api.onMessage("open-settings", () => { api.openSettings("performance"); return true; });
|
||||
|
||||
// Filter first, engine second: requests made while the engine builds pass
|
||||
// through (a few hundred milliseconds on first run), which beats holding
|
||||
|
|
|
|||
|
|
@ -75,7 +75,7 @@ h2 { margin: 0 0 8px; font-size: 11px; font-weight: 600; letter-spacing: .06em;
|
|||
<button class="btn quiet" id="refresh">Update now</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="note">Shield stops requests to known tracking and advertising hosts before they leave Theseus. It does not hide leftover empty ad boxes yet. If a site misbehaves, allow it here and reload.</div>
|
||||
<div class="note"><a href="#" id="toSettings" style="color:var(--blue)">Manage in Settings</a> · Shield stops requests to known tracking and advertising hosts before they leave Theseus. It does not hide leftover empty ad boxes yet. If a site misbehaves, allow it here and reload.</div>
|
||||
</div>
|
||||
<script>
|
||||
const $ = (id) => document.getElementById(id);
|
||||
|
|
@ -115,6 +115,7 @@ h2 { margin: 0 0 8px; font-size: 11px; font-weight: 600; letter-spacing: .06em;
|
|||
$("refresh").disabled = !!s.refreshing;
|
||||
}
|
||||
$("enabled").onclick = () => window.silentmode.invoke("set-enabled", { enabled: !(state && state.enabled) }).then(render);
|
||||
$("toSettings").onclick = (e) => { e.preventDefault(); window.silentmode.invoke("open-settings"); };
|
||||
$("refresh").onclick = () => { $("refresh").disabled = true; window.silentmode.invoke("refresh-lists").then((r) => { if (r && !r.ok) $("updated").textContent = "Update failed: " + (r.error || "unknown"); }); };
|
||||
window.silentmode.on("state", render);
|
||||
window.silentmode.invoke("state").then(render);
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
"author": "Silent Mode",
|
||||
"icon": "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCI+PGNpcmNsZSBjeD0iMTIiIGN5PSIxMiIgcj0iOSIgZmlsbD0iI2Y1YTUyNCIvPjxjaXJjbGUgY3g9IjkiIGN5PSI5IiByPSIxLjQiIGZpbGw9IiM3YzRhMWQiLz48Y2lyY2xlIGN4PSIxNSIgY3k9IjEwIiByPSIxLjIiIGZpbGw9IiM3YzRhMWQiLz48Y2lyY2xlIGN4PSIxMSIgY3k9IjE0LjUiIHI9IjEuMyIgZmlsbD0iIzdjNGExZCIvPjxwYXRoIGQ9Ik0xNS41IDE0LjUgbDUgNSBNMjAuNSAxNC41IGwtNSA1IiBzdHJva2U9IiNmZmYiIHN0cm9rZS13aWR0aD0iMi4yIiBzdHJva2UtbGluZWNhcD0icm91bmQiLz48L3N2Zz4=",
|
||||
"main": "index.js",
|
||||
"dock": "hidden",
|
||||
"capabilities": ["sidebar-panel", "page-inject"],
|
||||
"page-inject": {
|
||||
"preload": "inject.js",
|
||||
|
|
|
|||
|
|
@ -88,6 +88,7 @@ async function activate(a) {
|
|||
});
|
||||
// From the panel.
|
||||
api.onMessage("state", () => snapshot());
|
||||
api.onMessage("open-settings", () => { api.openSettings("performance"); return true; });
|
||||
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) => {
|
||||
|
|
|
|||
|
|
@ -69,7 +69,7 @@ h2 { margin: 0 0 8px; font-size: 11px; font-weight: 600; letter-spacing: .06em;
|
|||
</div>
|
||||
</div>
|
||||
<div class="note" id="rules"></div>
|
||||
<div class="note">Built on DuckDuckGo's autoconsent rules for hundreds of consent dialogs, with a fallback that looks for reject buttons on unknown ones. Some banners still need a click; if one keeps reappearing, exclude the site here.</div>
|
||||
<div class="note"><a href="#" id="toSettings" style="color:var(--warn)">Manage in Settings</a> · Built on DuckDuckGo's autoconsent rules for hundreds of consent dialogs, with a fallback that looks for reject buttons on unknown ones. Some banners still need a click; if one keeps reappearing, exclude the site here.</div>
|
||||
</div>
|
||||
<script>
|
||||
const $ = (id) => document.getElementById(id);
|
||||
|
|
@ -95,6 +95,7 @@ h2 { margin: 0 0 8px; font-size: 11px; font-weight: 600; letter-spacing: .06em;
|
|||
}
|
||||
const b = $("siteBtn"); if (b) b.onclick = () => window.silentmode.invoke("skip-site", { host: s.site.host, skipped: !s.site.skipped }).then(render);
|
||||
}
|
||||
$("toSettings").onclick = (e) => { e.preventDefault(); window.silentmode.invoke("open-settings"); };
|
||||
$("enabled").onclick = () => window.silentmode.invoke("set-enabled", { enabled: !(state && state.enabled) }).then(render);
|
||||
$("modes").addEventListener("change", (e) => { if (e.target.name === "mode") window.silentmode.invoke("set-mode", { mode: e.target.value }).then(render); });
|
||||
window.silentmode.on("state", render);
|
||||
|
|
|
|||
45
main.js
45
main.js
|
|
@ -393,6 +393,7 @@ const SETTINGS_DEFAULTS = {
|
|||
// buttons hidden from the toolbar via the dock's right-click menu.
|
||||
dockOrder: [],
|
||||
dockHidden: [],
|
||||
dockAutoHidden: [], // add-ons already started hidden once (manifest dock:"hidden")
|
||||
// Sidebar width in px. Adjusted by dragging the grip on the panel's left
|
||||
// edge; persisted across launches. Clamped to [200, 800] on load.
|
||||
sidebarWidth: 340,
|
||||
|
|
@ -2616,7 +2617,25 @@ function sidebarStatePayload() {
|
|||
},
|
||||
};
|
||||
}
|
||||
// Add-ons whose manifest says dock:"hidden" — Shield and Cookie Pop-ups,
|
||||
// whose settings live under Settings › Performance and whose panel is for
|
||||
// the details — start hidden from the toolbar dock. Done once per add-on,
|
||||
// so a user who shows the button keeps it.
|
||||
function autoHideQuietDock() {
|
||||
if (!addonHost) return;
|
||||
const seen = new Set(Array.isArray(settings.dockAutoHidden) ? settings.dockAutoHidden : []);
|
||||
const hidden = new Set(Array.isArray(settings.dockHidden) ? settings.dockHidden : []);
|
||||
let changed = false;
|
||||
for (const a of addonHost.getInstalled()) {
|
||||
const id = a.manifest && a.manifest.id;
|
||||
if (!id || a.manifest.dock !== "hidden" || seen.has(id)) continue;
|
||||
for (const k of dockKeys()) if (dockAddonId(k) === id) hidden.add(k);
|
||||
seen.add(id); changed = true;
|
||||
}
|
||||
if (changed) { settings.dockHidden = [...hidden]; settings.dockAutoHidden = [...seen]; saveSettings(); }
|
||||
}
|
||||
function emitSidebarState() {
|
||||
try { autoHideQuietDock(); } catch {}
|
||||
try { chrome?.webContents.send("sidebar-state", sidebarStatePayload()); } catch {}
|
||||
}
|
||||
function setSidebar(show, panelId) {
|
||||
|
|
@ -3151,6 +3170,15 @@ function createTab(initial, opts = {}) {
|
|||
});
|
||||
// A new document means a new (or no) web-app manifest; the chip follows.
|
||||
wc.on("did-navigate", () => { tab.webapp = null; });
|
||||
// A Settings (or add-on) tab the user navigates elsewhere is an ordinary tab
|
||||
// from then on; otherwise openSettingsTab keeps focusing a tab that no
|
||||
// longer shows Settings.
|
||||
wc.on("did-navigate", () => {
|
||||
if (!tab.settings && !tab.addonId) return;
|
||||
let u = ""; try { u = wc.getURL() || ""; } catch {}
|
||||
if (/^file:/i.test(u)) return;
|
||||
tab.settings = false; tab.addonId = null; tab.prov = null;
|
||||
});
|
||||
wc.on("did-navigate", () => { refreshTabUrl(tab); emitTabs(); historyAdd(tab.url, tab.title); });
|
||||
wc.on("did-navigate-in-page", () => { refreshTabUrl(tab); emitTabs(); historyAdd(tab.url, tab.title); });
|
||||
wc.on("did-navigate", () => { if (tab.id === activeId) notifyTabChange(); });
|
||||
|
|
@ -3527,6 +3555,10 @@ async function navigateTab(id, input) {
|
|||
// theseus://extensions/install/<id> typed or pasted into the address bar.
|
||||
const installId = installLinkId(q);
|
||||
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);
|
||||
if (settingsLink) { openSettingsTab(settingsLink[1] || ""); return; }
|
||||
// Local paths open as files — never BCNR, never a search.
|
||||
const fileUrl = localFileUrl(q);
|
||||
if (fileUrl) return loadLocalFile(t, id, fileUrl);
|
||||
|
|
@ -4877,6 +4909,19 @@ function openSettingsTab(section) {
|
|||
createTab(null, { settings: true, settingsSection: slug });
|
||||
}
|
||||
ipcMain.handle("open-settings", (_e, section) => openSettingsTab(section));
|
||||
// Settings › Performance drives Shield and Cookie Pop-ups through their
|
||||
// add-ons' own message handlers; only the Settings tab may call this.
|
||||
const isSettingsSender = (e) => tabs.some((t) => t.settings && t.view?.webContents === e.sender);
|
||||
ipcMain.handle("addon-invoke", (e, id, msg, payload) => {
|
||||
if (!isSettingsSender(e)) throw new Error("addon-invoke: settings only");
|
||||
if (!addonHost) throw new Error("no add-on host");
|
||||
return addonHost.dispatch(String(id || ""), String(msg || ""), payload, { from: "settings" });
|
||||
});
|
||||
ipcMain.handle("settings-open-panel", (e, panelId) => {
|
||||
if (!isSettingsSender(e)) throw new Error("settings-open-panel: settings only");
|
||||
setSidebar(true, String(panelId || ""));
|
||||
return true;
|
||||
});
|
||||
ipcMain.handle("toggle-site-info", (_e, rect) => {
|
||||
if (popVisible) return showPopover(false);
|
||||
if (rect) popPos = { x: Math.round(rect.x), y: Math.round(rect.y) };
|
||||
|
|
|
|||
|
|
@ -49,6 +49,10 @@ contextBridge.exposeInMainWorld("cfg", {
|
|||
removeAddon: (id) => ipcRenderer.invoke("addons-remove", id),
|
||||
// Install a staged signed update now (Settings › Extensions › "Update to vX"); no id = every staged extension.
|
||||
applyStagedAddons: (id) => ipcRenderer.invoke("addons-apply-staged", typeof id === "string" ? id : undefined),
|
||||
// Settings › Performance › Protections: talk to an add-on's own message
|
||||
// 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 || "")),
|
||||
openAddonsDir: () => ipcRenderer.invoke("addons-open-dir"),
|
||||
reloadAddons: () => ipcRenderer.invoke("addons-reload"),
|
||||
// Community extensions from theseus.x/extensions: the catalog (with what
|
||||
|
|
|
|||
|
|
@ -31,6 +31,10 @@
|
|||
.row .txt{flex:1}
|
||||
.row .t{font-weight:600}
|
||||
.row .d{color:var(--mut);font-size:13px;margin-top:2px}
|
||||
.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}
|
||||
/* 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%}
|
||||
|
|
@ -421,11 +425,25 @@
|
|||
<!-- PERFORMANCE -->
|
||||
<section id="performance" hidden>
|
||||
<h1>Performance</h1>
|
||||
<p class="lede">Keep Theseus light on resources.</p>
|
||||
<p class="lede">Keep Theseus light on resources, and keep trackers, ads and cookie banners out of your way.</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>
|
||||
<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>
|
||||
<div class="acts"><button class="btn small" id="shieldUpdate">Update rules</button><button class="btn small ghost" id="shieldPanel">Open panel</button></div></div>
|
||||
<label class="sw"><input type="checkbox" id="shieldOn"><span class="track"><span class="knob"></span></span></label>
|
||||
</div>
|
||||
<div class="row" id="consentCard" hidden>
|
||||
<div class="txt"><div class="t">Cookie pop-ups</div>
|
||||
<div class="d">Consent banners are answered for you before they get in the way. <span id="consentStats"></span></div>
|
||||
<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>
|
||||
<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>
|
||||
|
|
@ -677,6 +695,36 @@
|
|||
if (initSec) showSection(initSec);
|
||||
} catch {}
|
||||
|
||||
// Protections (Performance): Shield + Cookie Pop-ups, driven through the
|
||||
// add-ons' own message handlers. A card hides when its add-on is off.
|
||||
(function () {
|
||||
const inv = (id, msg, p) => (C.addonInvoke ? C.addonInvoke(id, msg, p) : Promise.reject(new Error("unavailable")));
|
||||
const el = (i) => document.getElementById(i);
|
||||
async function refresh() {
|
||||
try {
|
||||
const s = await inv("blocker", "state");
|
||||
el("shieldOn").checked = !!s.enabled;
|
||||
const L = s.lists || {};
|
||||
el("shieldStats").textContent = `${Number(s.blockedTotal || 0).toLocaleString()} blocked since install · rules ${L.source === "online" && L.updatedAt ? "updated " + new Date(L.updatedAt).toLocaleDateString() : "bundled"}${s.refreshing ? " · refreshing…" : ""}`;
|
||||
el("shieldCard").hidden = false;
|
||||
} catch { el("shieldCard").hidden = true; }
|
||||
try {
|
||||
const s = await inv("consent", "state");
|
||||
el("consentOn").checked = !!s.enabled; el("consentMode").value = s.mode || "optOut";
|
||||
el("consentStats").textContent = `${Number(s.handledTotal || 0).toLocaleString()} pop-ups answered since install.`;
|
||||
el("consentCard").hidden = false;
|
||||
} catch { el("consentCard").hidden = true; }
|
||||
}
|
||||
el("shieldOn").addEventListener("change", () => inv("blocker", "set-enabled", { enabled: el("shieldOn").checked }).then(refresh, refresh));
|
||||
el("shieldUpdate").addEventListener("click", () => { el("shieldUpdate").disabled = true; inv("blocker", "refresh-lists").catch(() => {}).then(() => { el("shieldUpdate").disabled = false; refresh(); }); });
|
||||
el("shieldPanel").addEventListener("click", () => C.openPanel && C.openPanel("blocker:main"));
|
||||
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();
|
||||
})();
|
||||
const TOGGLES = ["restoreSession", "backgroundThrottle", "blockCamera", "blockMicrophone", "hideMediaDevices",
|
||||
"clearCookiesOnQuit", "clearCacheOnQuit", "clearStorageOnQuit", "clearHistoryOnQuit"];
|
||||
C.get().then((s) => {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue