diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index e87195c..f391203 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -35,6 +35,8 @@ Read README.md first; this file is the short list of things that bite. - Security page URLs (~36k) live in `security/urls.json` and are checked by the worker; as DNR rules they would blow the 30k guarantee. - Icons are generated: edit `scripts/icons.mjs` and run it, never hand-edit the PNGs. +- Edge gets `dist/edge` (Chrome build + `src/icons-edge`, no middle finger). Edge rejected the + middle finger under policy 2.10; anything Edge sees, package or listing image, uses the polite set. ## Tests - `npm test` for the compiler and settings; `npm run test:e2e` for the real build in Chromium. diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 2629cab..87e00c7 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -26,6 +26,7 @@ jobs: - run: npm run build - run: npm test - run: node scripts/verify-load.mjs chrome + - run: node scripts/verify-load.mjs edge - run: npm run test:e2e - name: Firefox smoke test run: npm run test:firefox diff --git a/CHANGELOG.md b/CHANGELOG.md index b5a42f8..543db9b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,13 @@ ## [Unreleased] +## [2.0.3] - 2026-10-07 + +### Changed +- Edge gets its own build with a different hand on the icon (every finger up except the + middle one), after Edge Add-ons certification rejected the store logo. Chrome and Firefox + are unchanged. + ## [2.0.2] - 2026-09-29 ### Changed diff --git a/README.md b/README.md index 8add66a..50a4c57 100644 --- a/README.md +++ b/README.md @@ -98,6 +98,7 @@ src/ scriptlets/library.js MAIN-world scriptlets, embedded into each bucket by the build resources/ redirect stubs (GPT, gtag/GTM, analytics.js, noop files) icons/ toolbar icons per state, rendered by scripts/icons.mjs from assets/icons/ + icons-edge/ the same for the Edge build, without the middle finger (Edge policy 2.10) popup/, options/, warning/, shared/ test/unit, test/e2e, test/fixtures ``` @@ -153,6 +154,12 @@ The same scripts run locally: `npm run package`, then `publish:edge` with the variables above. `npm run store:status` shows what Chrome has. Listing text and images: `docs/store-listing.md` and `npm run store:art`. +Edge gets its own zip (`SilentBlock--edge.zip`, built from `dist/edge`) and its own images +(`release/store-art/edge/`). It is the Chrome build with a different hand on the icon: Edge +Add-ons rejected the middle finger under policy 2.10 on 2026-10-01. Edge also has no API for +listing images, and its publish API submits whatever listing is in the Partner Center draft, +so new Edge images go into the draft by hand BEFORE the tag is pushed. + ## Filter lists and licences | List | Licence | diff --git a/assets/icons/edge/caution.svg b/assets/icons/edge/caution.svg new file mode 100644 index 0000000..af2e957 --- /dev/null +++ b/assets/icons/edge/caution.svg @@ -0,0 +1 @@ +SilentBlock: Pushy site: pop-under, tab-under or nag wall dealt with (amber) diff --git a/assets/icons/edge/danger.svg b/assets/icons/edge/danger.svg new file mode 100644 index 0000000..6a0dfae --- /dev/null +++ b/assets/icons/edge/danger.svg @@ -0,0 +1 @@ +SilentBlock: Known malware, phishing or scam host (garnet) diff --git a/assets/icons/edge/idle.svg b/assets/icons/edge/idle.svg new file mode 100644 index 0000000..8c0aeec --- /dev/null +++ b/assets/icons/edge/idle.svg @@ -0,0 +1 @@ +SilentBlock: Idle, or nothing on this page needed blocking (slate blue) diff --git a/assets/icons/edge/paused.svg b/assets/icons/edge/paused.svg new file mode 100644 index 0000000..74eddf2 --- /dev/null +++ b/assets/icons/edge/paused.svg @@ -0,0 +1 @@ +SilentBlock: Paused on this site, or protection off (grey) diff --git a/assets/icons/edge/protected.svg b/assets/icons/edge/protected.svg new file mode 100644 index 0000000..662c590 --- /dev/null +++ b/assets/icons/edge/protected.svg @@ -0,0 +1 @@ +SilentBlock: Ads or trackers blocked (jade) diff --git a/docs/store-listing.md b/docs/store-listing.md index 65cb36e..11a88cf 100644 --- a/docs/store-listing.md +++ b/docs/store-listing.md @@ -3,7 +3,8 @@ One source for all three stores. The Firefox listing text is updated automatically on release (`scripts/publish/firefox.mjs`). Chrome and Edge have no API for listing text, so paste these in by hand when they change. Store images: `npm run store:art` renders them into -`release/store-art/`. +`release/store-art/` (Chrome and Firefox) and `release/store-art/edge/` (Edge, polite icon: +never upload the middle-finger set to Edge, it fails certification under policy 2.10). Store items: - Chrome Web Store: `accdpphckockpflaggbplikpmaknioao` (publisher b7b011da-ecfd-4ec1-8d99-c600f9c31543) diff --git a/package-lock.json b/package-lock.json index a60b97a..2e5e136 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "silentblock", - "version": "2.0.2", + "version": "2.0.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "silentblock", - "version": "2.0.2", + "version": "2.0.3", "devDependencies": { "@playwright/test": "^1.63.0", "selenium-webdriver": "^4.49.0" diff --git a/package.json b/package.json index 9cdfc9c..2621f45 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "silentblock", - "version": "2.0.2", + "version": "2.0.3", "private": true, "description": "Free, silent ad blocker for Chrome (MV3). No ads, no nags.", "type": "module", diff --git a/scripts/build.mjs b/scripts/build.mjs index f206ea8..918aa84 100644 --- a/scripts/build.mjs +++ b/scripts/build.mjs @@ -1,4 +1,4 @@ -// Builds dist/chrome and dist/firefox from src/ and the filter lists. +// Builds dist/chrome, dist/firefox and dist/edge from src/ and the filter lists. // // node scripts/build.mjs full build, selectors validated in headless Chromium // node scripts/build.mjs --no-validate skip browser validation (faster, CI without a browser) @@ -272,12 +272,14 @@ function manifestFor(target) { } const report = []; -for (const target of ['chrome', 'firefox']) { +// Edge is the Chrome build with the polite icons (src/icons-edge, see scripts/icons.mjs). +for (const target of ['chrome', 'firefox', 'edge']) { const dist = path.join(root, 'dist', target); await rm(dist, { recursive: true, force: true }); await mkdir(dist, { recursive: true }); for (const dir of ['background', 'popup', 'options', 'warning', 'content', 'shared', 'icons', 'resources']) { - await cp(path.join(root, 'src', dir), path.join(dist, dir), { recursive: true }); + const from = dir === 'icons' && target === 'edge' ? 'icons-edge' : dir; + await cp(path.join(root, 'src', from), path.join(dist, dir), { recursive: true }); } await rm(path.join(dist, 'shared/unwall-core.js'), { force: true }); for (const [rel, data] of Object.entries(out)) { diff --git a/scripts/icons.mjs b/scripts/icons.mjs index fb0e193..ed2df87 100644 --- a/scripts/icons.mjs +++ b/scripts/icons.mjs @@ -1,10 +1,14 @@ -// Renders the toolbar icons: one shield, five states. +// Renders the toolbar icons: one shield, five states, two hands. // node scripts/icons.mjs -// Writes assets/icons/.svg (sources) and src/icons/-.png (shipped). +// Writes assets/icons/.svg + src/icons/-.png (Chrome and Firefox), and +// assets/icons/edge/.svg + src/icons-edge/-.png (Edge). // // The shield is "Tidewater": faceted, with the hand cut clean through so the toolbar itself // makes the gesture. That only reads if the shield colour is mid-tone (a white finger on a // light toolbar, a dark one on a dark toolbar), so keep new colours in that band. +// +// Edge Add-ons rejected the middle finger (policy 2.10, store logo, 2026-10-01), so the Edge +// build gets the "polite" hand: every finger up except that one. import { writeFile, mkdir } from 'node:fs/promises'; import path from 'node:path'; import { chromium } from '@playwright/test'; @@ -20,43 +24,64 @@ export const STATES = { }; const SIZES = [16, 32, 48, 64, 128]; -// Hand in local coordinates, middle finger centred on x=32. The thumb grows out of the -// palm's lower-left along the index finger so it reads as one fist at 16px. -const HAND = [ - '', - '', - '', - '', - '', - '', - '', -].join(''); +// Hands in local coordinates, middle finger centred on x=32. +export const HANDS = { + // The thumb grows out of the palm's lower-left along the index finger so it reads as one + // fist at 16px. + classic: [ + '', + '', + '', + '', + '', + '', + '', + ].join(''), + // The classic fist with the fingers swapped: index, ring and pinky up, middle folded down. + // Same finger columns, palm and thumb as classic so the two read as one family. + polite: [ + '', + '', + '', + '', + '', + '', + '', + ].join(''), +}; +// Which hand each icon set uses, and where it goes. +const SETS = [ + { hand: 'classic', svgDir: 'assets/icons', pngDir: 'src/icons' }, + { hand: 'polite', svgDir: 'assets/icons/edge', pngDir: 'src/icons-edge' }, +]; const SHIELD = 'M64 10 L110 22 L104 80 L64 118 L24 80 L18 22 Z'; -export function shieldSvg({ body, facet }, title) { +export function shieldSvg({ body, facet }, title, hand = 'classic') { return `${title}` + `` + `` + `` + - `${HAND}` + + `${HANDS[hand]}` + `` + `\n`; } if (import.meta.url === `file:///${process.argv[1].replace(/\\/g, '/')}` || process.argv[1].endsWith('icons.mjs')) { - await mkdir(path.join(root, 'assets/icons'), { recursive: true }); - await mkdir(path.join(root, 'src/icons'), { recursive: true }); const browser = await chromium.launch(); const page = await browser.newPage(); - for (const [state, colours] of Object.entries(STATES)) { - const svg = shieldSvg(colours, `SilentBlock: ${colours.meaning}`); - await writeFile(path.join(root, `assets/icons/${state}.svg`), svg); - for (const size of SIZES) { - await page.setViewportSize({ width: size, height: size }); - await page.setContent(``); - await page.locator('img').screenshot({ path: path.join(root, `src/icons/${state}-${size}.png`), omitBackground: true }); + for (const { hand, svgDir, pngDir } of SETS) { + await mkdir(path.join(root, svgDir), { recursive: true }); + await mkdir(path.join(root, pngDir), { recursive: true }); + for (const [state, colours] of Object.entries(STATES)) { + const svg = shieldSvg(colours, `SilentBlock: ${colours.meaning}`, hand); + await writeFile(path.join(root, svgDir, `${state}.svg`), svg); + for (const size of SIZES) { + await page.setViewportSize({ width: size, height: size }); + await page.setContent(``); + await page.locator('img').screenshot({ path: path.join(root, pngDir, `${state}-${size}.png`), omitBackground: true }); + } + console.log(`${hand.padEnd(8)} ${state.padEnd(10)} ${colours.body} ${colours.meaning}`); } - console.log(`${state.padEnd(10)} ${colours.body} ${colours.meaning}`); } await browser.close(); } diff --git a/scripts/package.mjs b/scripts/package.mjs index 2c8c946..a26484f 100644 --- a/scripts/package.mjs +++ b/scripts/package.mjs @@ -1,4 +1,4 @@ -// Zips dist/chrome and dist/firefox into release/. manifest.json sits at the zip root, +// Zips dist/chrome, dist/firefox and dist/edge into release/. manifest.json sits at the zip root, // which is what the Chrome Web Store and addons.mozilla.org expect. // node scripts/package.mjs import { readdir, readFile, writeFile, mkdir, stat } from 'node:fs/promises'; @@ -77,7 +77,7 @@ function zip(files) { const outDir = path.join(root, 'release'); await mkdir(outDir, { recursive: true }); -for (const target of ['chrome', 'firefox']) { +for (const target of ['chrome', 'firefox', 'edge']) { const dir = path.join(root, 'dist', target); try { await stat(path.join(dir, 'manifest.json')); } catch { throw new Error(`dist/${target} missing: run npm run build first`); } const manifest = JSON.parse(await readFile(path.join(dir, 'manifest.json'), 'utf8')); diff --git a/scripts/publish/edge.mjs b/scripts/publish/edge.mjs index 309e697..da36e2d 100644 --- a/scripts/publish/edge.mjs +++ b/scripts/publish/edge.mjs @@ -1,4 +1,4 @@ -// Microsoft Edge Add-ons: upload the Chrome zip (Edge runs the same build) to the draft +// Microsoft Edge Add-ons: upload the Edge zip (the Chrome build with the polite icons) to the draft // submission, then publish it for certification. // EDGE_PRODUCT_ID=... EDGE_CLIENT_ID=... EDGE_API_KEY=... node scripts/publish/edge.mjs // Credentials: Partner Center > Microsoft Edge > Publish API. The product ID is the Partner @@ -24,7 +24,7 @@ async function waitFor(url, what) { const v = await version(); console.log(`edge: uploading v${v}`); const { res: upRes } = await call(`${API}/draft/package`, { - method: 'POST', headers: { ...auth, 'content-type': 'application/zip' }, body: await readFile(await zipFor('chrome')), + method: 'POST', headers: { ...auth, 'content-type': 'application/zip' }, body: await readFile(await zipFor('edge')), }, 'upload'); await waitFor(`${API}/draft/package/operations/${upRes.headers.get('location')}`, 'upload'); diff --git a/scripts/store-art.mjs b/scripts/store-art.mjs index 2f34e86..100ee10 100644 --- a/scripts/store-art.mjs +++ b/scripts/store-art.mjs @@ -1,5 +1,6 @@ -// Renders the store images from the real extension pages in dist/chrome. -// npm run store:art -> release/store-art/*.png +// Renders the store images from the real extension pages. +// npm run store:art -> release/store-art/*.png (Chrome Web Store, Firefox; from dist/chrome) +// release/store-art/edge/*.png (Edge Add-ons; from dist/edge, polite icons) // // The popup and options pages run with a stubbed `chrome` API so each shot shows a chosen // state. Hostnames use example.com and example.net (reserved for documentation) so no real @@ -9,145 +10,154 @@ import path from 'node:path'; import { chromium } from '@playwright/test'; const root = path.resolve(import.meta.dirname, '..'); -const dist = path.join(root, 'dist/chrome'); -const out = path.join(root, 'release/store-art'); const ORIGIN = 'https://silentblock.art'; -await mkdir(out, { recursive: true }); +const SETS = [ + { dist: 'dist/chrome', icons: 'assets/icons', out: 'release/store-art' }, + { dist: 'dist/edge', icons: 'assets/icons/edge', out: 'release/store-art/edge' }, +]; +const browser = await chromium.launch(); +for (const set of SETS) { + console.log(`${set.out}/`); + await render(path.join(root, set.dist), path.join(root, set.icons), path.join(root, set.out)); +} +await browser.close(); -const meta = JSON.parse(await readFile(path.join(dist, 'meta.json'), 'utf8')); -const svg = async (state) => `data:image/svg+xml;base64,${(await readFile(path.join(root, `assets/icons/${state}.svg`))).toString('base64')}`; -const ICON = Object.fromEntries(await Promise.all(['idle', 'protected', 'caution', 'danger', 'paused'].map(async (s) => [s, await svg(s)]))); +async function render(dist, iconDir, out) { + await mkdir(out, { recursive: true }); -function popupState(status, host) { - return { enabled: true, version: meta.version, supported: true, hostname: host, pausedBy: null, unwall: false, status }; -} -const STATES = { - protected: popupState({ blocked: 47, popups: 0, walls: 0, danger: 0, dangerHosts: [], level: 'protected', icon: 'protected' }, 'news.example.com'), - caution: popupState({ blocked: 31, popups: 2, walls: 1, danger: 0, dangerHosts: [], level: 'caution', icon: 'caution' }, 'streams.example.net'), - danger: popupState({ blocked: 12, popups: 0, walls: 0, danger: 3, dangerHosts: ['cdn-payload.example.net'], level: 'danger', icon: 'danger' }, 'free-downloads.example.net'), -}; -const stub = (popupData) => ` - window.chrome = { - runtime: { - sendMessage: async (m) => m.type === 'popup:get' ? ${JSON.stringify(popupData)} - : m.type === 'settings:get' ? { state: { enabled: true, allowlist: ['intranet.example.com'], categories: { ads: true, privacy: true, annoyances: true, security: true }, unwallSites: ['recipes.example.com'], badge: false, statusIcon: true }, meta: ${JSON.stringify(meta)} } - : {}, - openOptionsPage() {}, - }, - tabs: { query: async () => [{ id: 1 }], reload: async () => {}, getCurrent: async () => ({ id: 1 }) }, - };`; + const meta = JSON.parse(await readFile(path.join(dist, 'meta.json'), 'utf8')); + const svg = async (state) => `data:image/svg+xml;base64,${(await readFile(path.join(iconDir, `${state}.svg`))).toString('base64')}`; + const ICON = Object.fromEntries(await Promise.all(['idle', 'protected', 'caution', 'danger', 'paused'].map(async (s) => [s, await svg(s)]))); -const browser = await chromium.launch(); -async function capture(pagePath, { width, height, data, scale = 2, colorScheme = 'light' }) { - const ctx = await browser.newContext({ viewport: { width, height }, deviceScaleFactor: scale, colorScheme }); - await ctx.route(`${ORIGIN}/**`, async (route) => { - const rel = new URL(route.request().url()).pathname.slice(1); - const type = rel.endsWith('.html') ? 'text/html' : rel.endsWith('.css') ? 'text/css' : rel.endsWith('.js') ? 'text/javascript' : rel.endsWith('.png') ? 'image/png' : 'application/octet-stream'; - try { await route.fulfill({ body: await readFile(path.join(dist, rel)), contentType: type }); } catch { await route.fulfill({ status: 404 }); } - }); - const page = await ctx.newPage(); - if (data !== undefined) await page.addInitScript(stub(data)); - await page.goto(`${ORIGIN}/${pagePath}`); - await page.waitForTimeout(600); - const png = await page.screenshot({ fullPage: false }); - await ctx.close(); - return `data:image/png;base64,${png.toString('base64')}`; -} + function popupState(status, host) { + return { enabled: true, version: meta.version, supported: true, hostname: host, pausedBy: null, unwall: false, status }; + } + const STATES = { + protected: popupState({ blocked: 47, popups: 0, walls: 0, danger: 0, dangerHosts: [], level: 'protected', icon: 'protected' }, 'news.example.com'), + caution: popupState({ blocked: 31, popups: 2, walls: 1, danger: 0, dangerHosts: [], level: 'caution', icon: 'caution' }, 'streams.example.net'), + danger: popupState({ blocked: 12, popups: 0, walls: 0, danger: 3, dangerHosts: ['cdn-payload.example.net'], level: 'danger', icon: 'danger' }, 'free-downloads.example.net'), + }; + const stub = (popupData) => ` + window.chrome = { + runtime: { + sendMessage: async (m) => m.type === 'popup:get' ? ${JSON.stringify(popupData)} + : m.type === 'settings:get' ? { state: { enabled: true, allowlist: ['intranet.example.com'], categories: { ads: true, privacy: true, annoyances: true, security: true }, unwallSites: ['recipes.example.com'], badge: false, statusIcon: true }, meta: ${JSON.stringify(meta)} } + : {}, + openOptionsPage() {}, + }, + tabs: { query: async () => [{ id: 1 }], reload: async () => {}, getCurrent: async () => ({ id: 1 }) }, + };`; -const shots = { - protected: await capture('popup/popup.html?tabId=1', { width: 300, height: 330, data: STATES.protected }), - caution: await capture('popup/popup.html?tabId=1', { width: 300, height: 330, data: STATES.caution }), - danger: await capture('popup/popup.html?tabId=1', { width: 300, height: 330, data: STATES.danger }), - options: await capture('options/options.html', { width: 760, height: 900, data: null, scale: 1.5 }), - warning: await capture(`warning/warning.html?u=${encodeURIComponent('https://free-prize-claim.example.net/login')}&h=example.net`, { width: 900, height: 620, scale: 1.5 }), -}; + async function capture(pagePath, { width, height, data, scale = 2, colorScheme = 'light' }) { + const ctx = await browser.newContext({ viewport: { width, height }, deviceScaleFactor: scale, colorScheme }); + await ctx.route(`${ORIGIN}/**`, async (route) => { + const rel = new URL(route.request().url()).pathname.slice(1); + const type = rel.endsWith('.html') ? 'text/html' : rel.endsWith('.css') ? 'text/css' : rel.endsWith('.js') ? 'text/javascript' : rel.endsWith('.png') ? 'image/png' : 'application/octet-stream'; + try { await route.fulfill({ body: await readFile(path.join(dist, rel)), contentType: type }); } catch { await route.fulfill({ status: 404 }); } + }); + const page = await ctx.newPage(); + if (data !== undefined) await page.addInitScript(stub(data)); + await page.goto(`${ORIGIN}/${pagePath}`); + await page.waitForTimeout(600); + const png = await page.screenshot({ fullPage: false }); + await ctx.close(); + return `data:image/png;base64,${png.toString('base64')}`; + } -const FONT = `"Segoe UI", system-ui, -apple-system, Roboto, sans-serif`; -const frame = (w, h, inner) => `${inner}`; + const shots = { + protected: await capture('popup/popup.html?tabId=1', { width: 300, height: 330, data: STATES.protected }), + caution: await capture('popup/popup.html?tabId=1', { width: 300, height: 330, data: STATES.caution }), + danger: await capture('popup/popup.html?tabId=1', { width: 300, height: 330, data: STATES.danger }), + options: await capture('options/options.html', { width: 760, height: 900, data: null, scale: 1.5 }), + warning: await capture(`warning/warning.html?u=${encodeURIComponent('https://free-prize-claim.example.net/login')}&h=example.net`, { width: 900, height: 620, scale: 1.5 }), + }; -const pages = { - 'screenshot-1-blocks': frame(1280, 800, ` -
-
- -

Ads gone.
Silently.

-

Ads, trackers, pop-ups and cookie banners are blocked before they load. No nags, no account, nothing leaves your browser.

-
- -
`), - 'screenshot-2-status': frame(1280, 800, ` -
-

One look tells you what happened

-

The icon changes colour with every page.

-
- ${[['idle', 'Nothing to block'], ['protected', 'Ads and trackers blocked'], ['caution', 'Pushy site handled'], ['danger', 'Dangerous site blocked'], ['paused', 'Paused']].map(([s, l]) => ` -
- -

${l}

`).join('')} -
`), - 'screenshot-3-malware': frame(1280, 800, ` -
-
-

Stops dangerous sites before they load

-

Known malware, phishing and scam sites are checked on your device. Your browsing is never sent anywhere to be checked.

-
- -
`), - 'screenshot-4-pushy': frame(1280, 800, ` -
- -
-

Pop-ups and nag walls, handled

-

Pop-unders get closed, tab-unders get undone, and "turn off your ad blocker" walls come down with one click, or on their own on sites you choose.

-
-
`), - 'screenshot-5-settings': frame(1280, 800, ` -
-
-

Free. Open source. No strings.

-

More than 120,000 filters from EasyList, EasyPrivacy and uBlock Origin, bundled with each release. Pause any site with one switch.

-
- -
`), - 'promo-small-440x280': frame(440, 280, ` -
- -

SilentBlock

Ads gone. Silently.

-
`), - 'promo-marquee-1400x560': frame(1400, 560, ` -
- -

SilentBlock

-

Ads, trackers, pop-ups and malware, blocked. Silently.

- -
`), -}; + const FONT = `"Segoe UI", system-ui, -apple-system, Roboto, sans-serif`; + const frame = (w, h, inner) => `${inner}`; -const sizes = { 'promo-small-440x280': [440, 280], 'promo-marquee-1400x560': [1400, 560] }; -for (const [name, html] of Object.entries(pages)) { - const [w, h] = sizes[name] || [1280, 800]; - const page = await browser.newPage({ viewport: { width: w, height: h } }); - await page.setContent(html); - await page.waitForTimeout(200); - await page.screenshot({ path: path.join(out, `${name}.png`) }); - await page.close(); - console.log(`${name}.png ${w}x${h}`); -} -// Store icons: Edge wants 300x300, Chrome's listing icon is 128x128, Firefox asks for 32 and 64. -for (const [name, size] of [['logo-300', 300], ['icon-128', 128], ['icon-64', 64], ['icon-32', 32]]) { - const page = await browser.newPage({ viewport: { width: size, height: size } }); - await page.setContent(``); - await page.locator('img').screenshot({ path: path.join(out, `${name}.png`), omitBackground: true }); - await page.close(); - console.log(`${name}.png ${size}x${size}`); + const pages = { + 'screenshot-1-blocks': frame(1280, 800, ` +
+
+ +

Ads gone.
Silently.

+

Ads, trackers, pop-ups and cookie banners are blocked before they load. No nags, no account, nothing leaves your browser.

+
+ +
`), + 'screenshot-2-status': frame(1280, 800, ` +
+

One look tells you what happened

+

The icon changes colour with every page.

+
+ ${[['idle', 'Nothing to block'], ['protected', 'Ads and trackers blocked'], ['caution', 'Pushy site handled'], ['danger', 'Dangerous site blocked'], ['paused', 'Paused']].map(([s, l]) => ` +
+ +

${l}

`).join('')} +
`), + 'screenshot-3-malware': frame(1280, 800, ` +
+
+

Stops dangerous sites before they load

+

Known malware, phishing and scam sites are checked on your device. Your browsing is never sent anywhere to be checked.

+
+ +
`), + 'screenshot-4-pushy': frame(1280, 800, ` +
+ +
+

Pop-ups and nag walls, handled

+

Pop-unders get closed, tab-unders get undone, and "turn off your ad blocker" walls come down with one click, or on their own on sites you choose.

+
+
`), + 'screenshot-5-settings': frame(1280, 800, ` +
+
+

Free. Open source. No strings.

+

More than 120,000 filters from EasyList, EasyPrivacy and uBlock Origin, bundled with each release. Pause any site with one switch.

+
+ +
`), + 'promo-small-440x280': frame(440, 280, ` +
+ +

SilentBlock

Ads gone. Silently.

+
`), + 'promo-marquee-1400x560': frame(1400, 560, ` +
+ +

SilentBlock

+

Ads, trackers, pop-ups and malware, blocked. Silently.

+ +
`), + }; + + const sizes = { 'promo-small-440x280': [440, 280], 'promo-marquee-1400x560': [1400, 560] }; + for (const [name, html] of Object.entries(pages)) { + const [w, h] = sizes[name] || [1280, 800]; + const page = await browser.newPage({ viewport: { width: w, height: h } }); + await page.setContent(html); + await page.waitForTimeout(200); + await page.screenshot({ path: path.join(out, `${name}.png`) }); + await page.close(); + console.log(`${name}.png ${w}x${h}`); + } + // Store icons: Edge wants 300x300, Chrome's listing icon is 128x128, Firefox asks for 32 and 64. + for (const [name, size] of [['logo-300', 300], ['icon-128', 128], ['icon-64', 64], ['icon-32', 32]]) { + const page = await browser.newPage({ viewport: { width: size, height: size } }); + await page.setContent(``); + await page.locator('img').screenshot({ path: path.join(out, `${name}.png`), omitBackground: true }); + await page.close(); + console.log(`${name}.png ${size}x${size}`); + } } -await browser.close(); diff --git a/src/icons-edge/caution-128.png b/src/icons-edge/caution-128.png new file mode 100644 index 0000000..c534fe8 Binary files /dev/null and b/src/icons-edge/caution-128.png differ diff --git a/src/icons-edge/caution-16.png b/src/icons-edge/caution-16.png new file mode 100644 index 0000000..21be290 Binary files /dev/null and b/src/icons-edge/caution-16.png differ diff --git a/src/icons-edge/caution-32.png b/src/icons-edge/caution-32.png new file mode 100644 index 0000000..548ce00 Binary files /dev/null and b/src/icons-edge/caution-32.png differ diff --git a/src/icons-edge/caution-48.png b/src/icons-edge/caution-48.png new file mode 100644 index 0000000..764f923 Binary files /dev/null and b/src/icons-edge/caution-48.png differ diff --git a/src/icons-edge/caution-64.png b/src/icons-edge/caution-64.png new file mode 100644 index 0000000..d56fa97 Binary files /dev/null and b/src/icons-edge/caution-64.png differ diff --git a/src/icons-edge/danger-128.png b/src/icons-edge/danger-128.png new file mode 100644 index 0000000..26cbc13 Binary files /dev/null and b/src/icons-edge/danger-128.png differ diff --git a/src/icons-edge/danger-16.png b/src/icons-edge/danger-16.png new file mode 100644 index 0000000..fe2f57d Binary files /dev/null and b/src/icons-edge/danger-16.png differ diff --git a/src/icons-edge/danger-32.png b/src/icons-edge/danger-32.png new file mode 100644 index 0000000..51fac3e Binary files /dev/null and b/src/icons-edge/danger-32.png differ diff --git a/src/icons-edge/danger-48.png b/src/icons-edge/danger-48.png new file mode 100644 index 0000000..b2c9eae Binary files /dev/null and b/src/icons-edge/danger-48.png differ diff --git a/src/icons-edge/danger-64.png b/src/icons-edge/danger-64.png new file mode 100644 index 0000000..93b0f64 Binary files /dev/null and b/src/icons-edge/danger-64.png differ diff --git a/src/icons-edge/idle-128.png b/src/icons-edge/idle-128.png new file mode 100644 index 0000000..c964ee9 Binary files /dev/null and b/src/icons-edge/idle-128.png differ diff --git a/src/icons-edge/idle-16.png b/src/icons-edge/idle-16.png new file mode 100644 index 0000000..d2c954a Binary files /dev/null and b/src/icons-edge/idle-16.png differ diff --git a/src/icons-edge/idle-32.png b/src/icons-edge/idle-32.png new file mode 100644 index 0000000..9129a6b Binary files /dev/null and b/src/icons-edge/idle-32.png differ diff --git a/src/icons-edge/idle-48.png b/src/icons-edge/idle-48.png new file mode 100644 index 0000000..ba1e3a8 Binary files /dev/null and b/src/icons-edge/idle-48.png differ diff --git a/src/icons-edge/idle-64.png b/src/icons-edge/idle-64.png new file mode 100644 index 0000000..28bea5b Binary files /dev/null and b/src/icons-edge/idle-64.png differ diff --git a/src/icons-edge/paused-128.png b/src/icons-edge/paused-128.png new file mode 100644 index 0000000..e848cfe Binary files /dev/null and b/src/icons-edge/paused-128.png differ diff --git a/src/icons-edge/paused-16.png b/src/icons-edge/paused-16.png new file mode 100644 index 0000000..d4cb249 Binary files /dev/null and b/src/icons-edge/paused-16.png differ diff --git a/src/icons-edge/paused-32.png b/src/icons-edge/paused-32.png new file mode 100644 index 0000000..25bb279 Binary files /dev/null and b/src/icons-edge/paused-32.png differ diff --git a/src/icons-edge/paused-48.png b/src/icons-edge/paused-48.png new file mode 100644 index 0000000..bc67ffb Binary files /dev/null and b/src/icons-edge/paused-48.png differ diff --git a/src/icons-edge/paused-64.png b/src/icons-edge/paused-64.png new file mode 100644 index 0000000..3bbec1d Binary files /dev/null and b/src/icons-edge/paused-64.png differ diff --git a/src/icons-edge/protected-128.png b/src/icons-edge/protected-128.png new file mode 100644 index 0000000..57a2ff0 Binary files /dev/null and b/src/icons-edge/protected-128.png differ diff --git a/src/icons-edge/protected-16.png b/src/icons-edge/protected-16.png new file mode 100644 index 0000000..af7fe24 Binary files /dev/null and b/src/icons-edge/protected-16.png differ diff --git a/src/icons-edge/protected-32.png b/src/icons-edge/protected-32.png new file mode 100644 index 0000000..3b36c24 Binary files /dev/null and b/src/icons-edge/protected-32.png differ diff --git a/src/icons-edge/protected-48.png b/src/icons-edge/protected-48.png new file mode 100644 index 0000000..53d9ac4 Binary files /dev/null and b/src/icons-edge/protected-48.png differ diff --git a/src/icons-edge/protected-64.png b/src/icons-edge/protected-64.png new file mode 100644 index 0000000..0ea8d6f Binary files /dev/null and b/src/icons-edge/protected-64.png differ