Theseus had no content blocking at all. Shield blocks requests to known tracking and advertising hosts on every site, using EasyList and EasyPrivacy through Ghostery's adblocker engine (the matcher those lists are written for). The lists ship inside the add-on so blocking works from the first launch, offline; the compiled engine is cached under the add-on's data dir (a 22 ms load instead of a 500 ms parse), and the lists refresh from their publishers about once a day. The panel shows what was stopped on the current page, a one-click allow for the site, the global switch, the running total and the rule versions with an "Update now". Network filters only for now: a blocked request never leaves the browser, but leftover empty ad boxes are not hidden yet. Host side: a "request-filter" capability. Chromium allows one onBeforeRequest listener per session, so main owns it and consults the add-ons' filters; a top-level navigation is never blocked, only http(s) subresources are offered. api.tabs (active tab and a change event) lets the panel show per-site numbers without seeing page content.
124 lines
8 KiB
HTML
124 lines
8 KiB
HTML
<!doctype html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<title>Shield</title>
|
|
<style>
|
|
:root { color-scheme: light dark;
|
|
--bg:#0e131c; --panel:#141a24; --panel2:#191f2b; --line:rgba(255,255,255,.09);
|
|
--ink:#e7eaf1; --mut:#8b98a9; --dim:#5e6678; --acid:#0ac18e; --blue:#4c9eff; --warn:#f5a524; --danger:#ff5b5b; }
|
|
@media (prefers-color-scheme: light) {
|
|
:root { --bg:#f8faff; --panel:#ffffff; --panel2:#eff3fb; --line:rgba(0,0,0,.10);
|
|
--ink:#1a1f2b; --mut:#5c6577; --dim:#8a93a5; --blue:#2b6fd6; }
|
|
}
|
|
* { box-sizing: border-box; }
|
|
html, body { margin: 0; height: 100%; }
|
|
body { background: var(--bg); color: var(--ink); font: 13px/1.5 system-ui, -apple-system, Segoe UI, Roboto, sans-serif; display: flex; flex-direction: column; }
|
|
header { display: flex; align-items: center; gap: 8px; padding: 12px 14px; border-bottom: 1px solid var(--line); background: var(--panel); }
|
|
header h1 { margin: 0; font-size: 14px; font-weight: 600; }
|
|
header .pill { margin-left: auto; }
|
|
.pill { display: inline-flex; align-items: center; gap: 6px; padding: 3px 9px; border-radius: 999px;
|
|
font-size: 11px; font-weight: 600; letter-spacing: .04em; text-transform: uppercase;
|
|
background: rgba(139,152,169,.14); color: var(--mut); }
|
|
.pill.on { color: #0b0e14; background: var(--acid); }
|
|
.pill.off { color: #fff; background: var(--danger); }
|
|
.pill .dot { width: 6px; height: 6px; border-radius: 50%; background: currentColor; }
|
|
.body { flex: 1; overflow-y: auto; padding: 14px; display: flex; flex-direction: column; gap: 12px; }
|
|
.card { border: 1px solid var(--line); background: var(--panel); border-radius: 14px; padding: 16px; }
|
|
.site { display: flex; flex-direction: column; align-items: center; gap: 6px; text-align: center; }
|
|
.site .host { font: 600 13px/1.3 ui-monospace, "Cascadia Code", Consolas, monospace; color: var(--mut); overflow-wrap: anywhere; }
|
|
.site .num { font-size: 40px; font-weight: 700; line-height: 1.1; color: var(--blue); font-variant-numeric: tabular-nums; }
|
|
.site .num.allowed { color: var(--dim); }
|
|
.site .lbl { color: var(--mut); }
|
|
.site .sub { color: var(--dim); font-size: 11.5px; }
|
|
.btn { border: 1px solid var(--line); background: var(--panel2); color: var(--ink); padding: 9px 16px; border-radius: 10px;
|
|
font: inherit; font-weight: 600; cursor: pointer; }
|
|
.btn:hover { border-color: rgb(from var(--blue) r g b / .5); }
|
|
.btn.primary { background: var(--blue); color: #fff; border-color: transparent; }
|
|
.btn.quiet { background: transparent; }
|
|
.btn:disabled { opacity: .5; cursor: default; }
|
|
.btn.wide { margin-top: 10px; min-width: 200px; }
|
|
.row { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
|
|
.row + .row { margin-top: 10px; padding-top: 10px; border-top: 1px solid var(--line); }
|
|
.row .k { font-weight: 600; }
|
|
.row .d { color: var(--dim); font-size: 11.5px; }
|
|
.row .v { color: var(--mut); font-variant-numeric: tabular-nums; text-align: right; }
|
|
.switch { position: relative; width: 40px; height: 22px; border-radius: 11px; background: var(--dim); border: none; cursor: pointer; flex: none; transition: background .15s; }
|
|
.switch::after { content: ""; position: absolute; top: 2px; left: 2px; width: 18px; height: 18px; border-radius: 50%; background: #fff; transition: left .15s; }
|
|
.switch.on { background: var(--acid); } .switch.on::after { left: 20px; }
|
|
.note { color: var(--dim); font-size: 11.5px; line-height: 1.45; }
|
|
.note.warn { color: var(--warn); }
|
|
h2 { margin: 0 0 8px; font-size: 11px; font-weight: 600; letter-spacing: .06em; text-transform: uppercase; color: var(--dim); }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<header><span>🛡</span><h1>Shield</h1><span class="pill" id="pill"><span class="dot"></span><span id="pillText">…</span></span></header>
|
|
<div class="body">
|
|
<div class="card site" id="siteCard">
|
|
<div class="lbl" id="siteLbl">Open a site to see what Shield stops there.</div>
|
|
</div>
|
|
<div class="card">
|
|
<div class="row">
|
|
<div><div class="k">Block trackers and ads</div><div class="d">On every site, using EasyList and EasyPrivacy.</div></div>
|
|
<button class="switch" id="enabled" role="switch" aria-checked="true" title="Turn Shield on or off"></button>
|
|
</div>
|
|
<div class="row">
|
|
<div><div class="k">Blocked since install</div><div class="d">Requests that never left the browser.</div></div>
|
|
<div class="v" id="total">0</div>
|
|
</div>
|
|
</div>
|
|
<div class="card">
|
|
<h2>Rules</h2>
|
|
<div id="lists"></div>
|
|
<div class="row" style="margin-top:12px; padding-top:12px; border-top:1px solid var(--line)">
|
|
<div class="note" id="updated"></div>
|
|
<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>
|
|
<script>
|
|
const $ = (id) => document.getElementById(id);
|
|
const esc = (s) => String(s ?? "").replace(/[&<>"']/g, (c) => ({ "&":"&","<":"<",">":">",'"':""","'":"'" })[c]);
|
|
const ago = (t) => { if (!t) return "bundled with the add-on"; const m = Math.round((Date.now() - t) / 60000); if (m < 2) return "updated just now"; if (m < 90) return `updated ${m} min ago`; const h = Math.round(m / 60); if (h < 36) return `updated ${h} h ago`; return `updated ${Math.round(h / 24)} days ago`; };
|
|
let state = null, hintHost = null;
|
|
function render(s) {
|
|
state = s;
|
|
const on = s.enabled && s.ready;
|
|
$("pill").className = "pill " + (s.enabled ? (s.ready ? "on" : "") : "off");
|
|
$("pillText").textContent = !s.enabled ? "Off" : !s.ready ? "Loading" : "On";
|
|
$("enabled").classList.toggle("on", s.enabled); $("enabled").setAttribute("aria-checked", String(s.enabled));
|
|
$("total").textContent = Number(s.blockedTotal || 0).toLocaleString();
|
|
const card = $("siteCard");
|
|
if (!s.site) {
|
|
card.innerHTML = `<div class="lbl">Open a site to see what Shield stops there.</div>`;
|
|
} else if (s.site.allowed) {
|
|
card.innerHTML = `<div class="host">${esc(s.site.host)}</div><div class="num allowed">—</div><div class="lbl">Allowed: nothing is blocked on this site.</div>
|
|
<button class="btn primary wide" id="siteBtn">Block on this site</button>`;
|
|
} else {
|
|
card.innerHTML = `<div class="host">${esc(s.site.host)}</div><div class="num">${on ? Number(s.site.blocked).toLocaleString() : "—"}</div>
|
|
<div class="lbl">${on ? (s.site.blocked === 1 ? "request blocked on this page" : "requests blocked on this page") : "Shield is off"}</div>
|
|
<div class="sub">${esc(s.site.requests)} requests seen</div>
|
|
<button class="btn wide" id="siteBtn">Allow this site</button>`;
|
|
}
|
|
const btn = $("siteBtn");
|
|
if (btn) btn.onclick = async () => {
|
|
const next = !s.site.allowed;
|
|
hintHost = s.site.host;
|
|
await window.silentmode.invoke("allow-site", { host: s.site.host, allowed: next });
|
|
};
|
|
if (s.site && hintHost === s.site.host) card.insertAdjacentHTML("beforeend", `<div class="sub">Reload the page to apply.</div>`);
|
|
else hintHost = null;
|
|
const L = s.lists || { versions: {}, titles: {} };
|
|
$("lists").innerHTML = Object.keys(L.titles || {}).map((id) => `<div class="row"><div class="k">${esc(L.titles[id])}</div><div class="v">${esc(L.versions[id] || "—")}</div></div>`).join("");
|
|
$("updated").textContent = (L.source === "online" ? "Rules " + ago(L.updatedAt) : "Rules bundled with the add-on") + (s.refreshing ? " · refreshing…" : "");
|
|
$("refresh").disabled = !!s.refreshing;
|
|
}
|
|
$("enabled").onclick = () => window.silentmode.invoke("set-enabled", { enabled: !(state && state.enabled) }).then(render);
|
|
$("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);
|
|
window.silentmode.onVisibility((v) => { if (v) window.silentmode.invoke("state").then(render); });
|
|
</script>
|
|
</body>
|
|
</html>
|