[mv3] Bring back element zapper

This commit is contained in:
Raymond Hill 2025-03-21 13:23:54 -04:00
parent 0f2a374585
commit ab458b492a
No known key found for this signature in database
GPG key ID: 25E1490B761470C2
12 changed files with 856 additions and 37 deletions

View file

@ -12,6 +12,11 @@
"service_worker": "/js/background.js",
"type": "module"
},
"commands": {
"enter-zapper-mode": {
"description": "__MSG_zapperTipEnter__"
}
},
"declarative_net_request": {
"rule_resources": [
]
@ -51,6 +56,14 @@
"<all_urls>"
],
"use_dynamic_url": true
},
{
"resources": [
"/zapper-ui.html"
],
"matches": [
"<all_urls>"
]
}
]
}

View file

@ -278,5 +278,13 @@
"strictblockProceed": {
"message": "Proceed",
"description": "A button to navigate to the blocked page"
},
"zapperTipEnter": {
"message": "Enter element zapper mode",
"description": "Tooltip for the button used to enter zapper mode"
},
"zapperTipQuit": {
"message": "Exit element zapper mode",
"description": "Tooltip for the button used to exit zapper mode"
}
}

View file

@ -155,13 +155,13 @@ body.needReload #refresh {
}
.toolRibbon {
align-items: center;
align-items: start;
background-color: var(--popup-toolbar-surface);
display: grid;
grid-auto-columns: 1fr;
grid-auto-flow: column;
grid-template: auto / repeat(5, 1fr);
justify-items: center;
grid-template: auto / repeat(4, 1fr);
justify-items: stretch;
white-space: normal;
}
.toolRibbon .tool {

View file

@ -0,0 +1,50 @@
html#ubol-zapper,
#ubol-zapper body {
background: transparent;
cursor: not-allowed;
height: 100vh;
height: 100svh;
margin: 0;
overflow: hidden;
width: 100vw;
}
#ubol-zapper :focus {
outline: none;
}
#ubol-zapper aside {
background-color: var(--surface-1);
box-sizing: border-box;
cursor: default;
display: flex;
flex-direction: column;
position: fixed;
z-index: 100;
}
#ubol-zapper aside > #quit {
fill: none;
height: 4em;
stroke: var(--ink-1);
stroke-width: 3px;
width: 4em;
}
#ubol-zapper svg#sea {
cursor: crosshair;
box-sizing: border-box;
height: 100%;
left: 0;
position: absolute;
top: 0;
width: 100%;
}
#ubol-zapper svg#sea > path:first-child {
fill: rgba(0,0,0,0.5);
fill-rule: evenodd;
}
#ubol-zapper svg#sea > path + path {
stroke: #FF0;
stroke-width: 0.5px;
fill: rgba(255,255,63,0.20);
}
#ubol-zapper #quit:hover {
background-color: var(--surface-2)
}

View file

@ -198,6 +198,20 @@ function onMessage(request, sender, callback) {
return false;
}
case 'removeCSS': {
const tabId = sender?.tab?.id ?? false;
const frameId = sender?.frameId ?? false;
if ( tabId === false || frameId === false ) { return; }
browser.scripting.removeCSS({
css: request.css,
origin: 'USER',
target: { tabId, frameIds: [ frameId ] },
}).catch(reason => {
console.log(reason);
});
return false;
}
default:
break;
}
@ -414,6 +428,23 @@ function onMessage(request, sender, callback) {
/******************************************************************************/
function onCommand(command, tab) {
switch ( command ) {
case 'enter-zapper-mode': {
if ( browser.scripting === undefined ) { return; }
browser.scripting.executeScript({
files: [ '/js/scripting/zapper.js' ],
target: { tabId: tab.id },
});
break;
}
default:
break;
}
}
/******************************************************************************/
async function start() {
await loadRulesetConfig();
@ -466,8 +497,6 @@ async function start() {
});
}
runtime.onMessage.addListener(onMessage);
if ( process.firstRun ) {
const enableOptimal = await hasOmnipotence();
if ( enableOptimal ) {
@ -493,27 +522,56 @@ async function start() {
if ( process.wakeupRun === false ) {
adminReadEx('disabledFeatures');
}
browser.permissions.onRemoved.addListener(onPermissionsRemoved);
browser.permissions.onAdded.addListener(onPermissionsAdded);
}
// https://github.com/uBlockOrigin/uBOL-home/issues/199
// Force a restart of the extension once when an "internal error" occurs
start().then(( ) => {
localRemove('goodStart');
return false;
}).catch(reason => {
console.trace(reason);
if ( process.wakeupRun ) { return; }
return localRead('goodStart').then(goodStart => {
if ( goodStart === false ) {
localRemove('goodStart');
return false;
}
return localWrite('goodStart', false).then(( ) => true);
/******************************************************************************/
const isFullyInitialized = new Promise(resolve => {
// https://github.com/uBlockOrigin/uBOL-home/issues/199
// Force a restart of the extension once when an "internal error" occurs
start().then(( ) => {
localRemove('goodStart');
return false;
}).catch(reason => {
console.trace(reason);
if ( process.wakeupRun ) { return; }
return localRead('goodStart').then(goodStart => {
if ( goodStart === false ) {
localRemove('goodStart');
return false;
}
return localWrite('goodStart', false).then(( ) => true);
});
}).then(restart => {
if ( restart !== true ) { return; }
runtime.reload();
}).finally(( ) => {
resolve(true);
});
});
runtime.onMessage.addListener((request, sender, callback) => {
isFullyInitialized.then(( ) => {
const r = onMessage(request, sender, callback);
if ( r !== true ) { callback(); }
});
return true;
});
browser.permissions.onRemoved.addListener((...args) => {
isFullyInitialized.then(( ) => {
onPermissionsRemoved(...args);
});
});
browser.permissions.onAdded.addListener((...args) => {
isFullyInitialized.then(( ) => {
onPermissionsAdded(...args);
});
});
browser.commands.onCommand.addListener((...args) => {
isFullyInitialized.then(( ) => {
onCommand(...args);
});
}).then(restart => {
if ( restart !== true ) { return; }
runtime.reload();
});

View file

@ -319,6 +319,17 @@ dom.on('[data-i18n-title="popupTipDashboard"]', 'click', ev => {
/******************************************************************************/
dom.on('#gotoZapper', 'click', ( ) => {
if ( browser.scripting === undefined ) { return; }
browser.scripting.executeScript({
files: [ '/js/scripting/zapper.js' ],
target: { tabId: currentTab.id },
});
self.close();
});
/******************************************************************************/
async function init() {
const [ tab ] = await browser.tabs.query({
active: true,
@ -362,9 +373,10 @@ async function init() {
isNaN(currentTab.id) === false
);
dom.cl.toggle('#reportFilterIssue', 'enabled',
/^https?:\/\//.test(url?.href)
);
const isNetworkPage = url.protocol === 'http:' || url.protocol === 'https:';
dom.cl.toggle('#reportFilterIssue', 'enabled', isNetworkPage );
dom.cl.toggle('#gotoZapper', 'enabled', isNetworkPage);
const parent = qs$('#rulesetStats');
for ( const details of popupPanelData.rulesetDetails || [] ) {

View file

@ -0,0 +1,409 @@
/*******************************************************************************
uBlock Origin - a comprehensive, efficient content blocker
Copyright (C) 2025-present Raymond Hill
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see {http://www.gnu.org/licenses/}.
Home: https://github.com/gorhill/uBlock
*/
(async ( ) => {
/******************************************************************************/
const zapper = self.uBOLZapper = self.uBOLZapper || {};
if ( zapper.injected ) { return; }
zapper.injected = true;
/******************************************************************************/
const sendMessage = msg => {
try {
chrome.runtime.sendMessage(msg).catch(( ) => { });
} catch {
}
};
/******************************************************************************/
const zapperSecret = (( ) => {
let secret = String.fromCharCode((Math.random() * 26) + 97);
do {
secret += (Math.floor(Math.random() * 2147483647) + 2147483647)
.toString(36)
.slice(2);
} while ( secret.length < 8 );
return secret;
})();
const zapperCSSStyle = [
'background: transparent',
'border: 0',
'border-radius: 0',
'box-shadow: none',
'color-scheme: light dark',
'display: block',
'filter: none',
'height: 100vh',
' height: 100svh',
'left: 0',
'margin: 0',
'max-height: none',
'max-width: none',
'min-height: unset',
'min-width: unset',
'opacity: 1',
'outline: 0',
'padding: 0',
'pointer-events: auto',
'position: fixed',
'top: 0',
'transform: none',
'visibility: hidden',
'width: 100%',
'z-index: 2147483647',
''
].join(' !important;\n');
const zapperCSS = `
:root > [${zapperSecret}] {
${zapperCSSStyle}
}
:root > [${zapperSecret}-loaded] {
visibility: visible !important;
}
:root [${zapperSecret}-click] {
pointer-events: none !important;
}
`;
sendMessage({ what: 'insertCSS', css: zapperCSS });
/******************************************************************************/
const getElementBoundingClientRect = function(elem) {
let rect = typeof elem.getBoundingClientRect === 'function'
? elem.getBoundingClientRect()
: { height: 0, left: 0, top: 0, width: 0 };
// https://github.com/gorhill/uBlock/issues/1024
// Try not returning an empty bounding rect.
if ( rect.width !== 0 && rect.height !== 0 ) {
return rect;
}
if ( elem.shadowRoot instanceof DocumentFragment ) {
return getElementBoundingClientRect(elem.shadowRoot);
}
let left = rect.left,
right = left + rect.width,
top = rect.top,
bottom = top + rect.height;
for ( const child of elem.children ) {
rect = getElementBoundingClientRect(child);
if ( rect.width === 0 || rect.height === 0 ) { continue; }
if ( rect.left < left ) { left = rect.left; }
if ( rect.right > right ) { right = rect.right; }
if ( rect.top < top ) { top = rect.top; }
if ( rect.bottom > bottom ) { bottom = rect.bottom; }
}
return {
bottom,
height: bottom - top,
left,
right,
top,
width: right - left
};
};
/******************************************************************************/
const highlightElement = function(elem) {
if ( elem !== true ) {
if ( elem !== undefined ) {
if ( elem === highlightElement.current ) { return; }
if ( elem !== zapperFrame ) {
highlightElement.current = elem;
}
}
}
elem = highlightElement.current;
const ow = self.innerWidth;
const oh = self.innerHeight;
const islands = [];
if ( elem !== null ) {
const rect = getElementBoundingClientRect(elem);
// Ignore offscreen areas
if (
rect.left <= ow && rect.top <= oh &&
rect.left + rect.width >= 0 && rect.top + rect.height >= 0
) {
islands.push(
`M${rect.left} ${rect.top}h${rect.width}v${rect.height}h-${rect.width}z`
);
}
}
zapperFramePort.postMessage({
what: 'svgPaths',
ocean: `M0 0h${ow}v${oh}h-${ow}z`,
islands: islands.join(''),
});
};
highlightElement.current = null;
/******************************************************************************/
const elementFromPoint = (( ) => {
let lastX, lastY;
return (x, y) => {
if ( x !== undefined ) {
lastX = x; lastY = y;
} else if ( lastX !== undefined ) {
x = lastX; y = lastY;
} else {
return null;
}
if ( !zapperFrame ) { return null; }
const magicAttr = `${zapperSecret}-click`;
zapperFrame.setAttribute(magicAttr, '');
let elem = document.elementFromPoint(x, y);
if ( elem === document.body || elem === document.documentElement ) {
elem = null;
}
// https://github.com/uBlockOrigin/uBlock-issues/issues/380
zapperFrame.removeAttribute(magicAttr);
return elem;
};
})();
/******************************************************************************/
const highlightElementAtPoint = function(mx, my) {
const elem = elementFromPoint(mx, my);
highlightElement(elem);
};
/******************************************************************************/
// https://www.reddit.com/r/uBlockOrigin/comments/bktxtb/scrolling_doesnt_work/emn901o
// Override 'fixed' position property on body element if present.
// With touch-driven devices, first highlight the element and remove only
// when tapping again the highlighted area.
const zapElementAtPoint = function(mx, my, options) {
if ( options.highlight ) {
const elem = elementFromPoint(mx, my);
if ( elem ) {
highlightElement(elem);
}
return;
}
let elemToRemove = highlightElement.current;
if ( elemToRemove === null && mx !== undefined ) {
elemToRemove = elementFromPoint(mx, my);
}
if ( elemToRemove instanceof Element === false ) { return; }
const getStyleValue = (elem, prop) => {
const style = window.getComputedStyle(elem);
return style ? style[prop] : '';
};
// Heuristic to detect scroll-locking: remove such lock when detected.
let maybeScrollLocked = elemToRemove.shadowRoot instanceof DocumentFragment;
if ( maybeScrollLocked === false ) {
let elem = elemToRemove;
do {
maybeScrollLocked =
parseInt(getStyleValue(elem, 'zIndex'), 10) >= 1000 ||
getStyleValue(elem, 'position') === 'fixed';
elem = elem.parentElement;
} while ( elem !== null && maybeScrollLocked === false );
}
if ( maybeScrollLocked ) {
const doc = document;
if ( getStyleValue(doc.body, 'overflowY') === 'hidden' ) {
doc.body.style.setProperty('overflow', 'auto', 'important');
}
if ( getStyleValue(doc.body, 'position') === 'fixed' ) {
doc.body.style.setProperty('position', 'initial', 'important');
}
if ( getStyleValue(doc.documentElement, 'position') === 'fixed' ) {
doc.documentElement.style.setProperty('position', 'initial', 'important');
}
if ( getStyleValue(doc.documentElement, 'overflowY') === 'hidden' ) {
doc.documentElement.style.setProperty('overflow', 'auto', 'important');
}
}
elemToRemove.remove();
highlightElementAtPoint(mx, my);
};
/******************************************************************************/
const onKeyPressed = function(ev) {
// Delete
if (
(ev.key === 'Delete' || ev.key === 'Backspace') ) {
ev.stopPropagation();
ev.preventDefault();
zapElementAtPoint();
return;
}
// Esc
if ( ev.key === 'Escape' || ev.which === 27 ) {
ev.stopPropagation();
ev.preventDefault();
quitZapper();
return;
}
};
/******************************************************************************/
// https://github.com/chrisaljoudi/uBlock/issues/190
// May need to dynamically adjust the height of the overlay + new position
// of highlighted elements.
const onViewportChanged = function() {
highlightElement(true);
};
/******************************************************************************/
const startZapper = function() {
zapperFrame.focus();
self.addEventListener('scroll', onViewportChanged, { passive: true });
self.addEventListener('resize', onViewportChanged, { passive: true });
self.addEventListener('keydown', onKeyPressed, true);
};
/******************************************************************************/
const quitZapper = function() {
self.removeEventListener('scroll', onViewportChanged, { passive: true });
self.removeEventListener('resize', onViewportChanged, { passive: true });
self.removeEventListener('keydown', onKeyPressed, true);
if ( zapperFramePort ) {
zapperFramePort.close();
zapperFramePort.onmessage = null;
zapperFramePort.onmessageerror = null;
zapperFramePort = null;
}
if ( zapperFrame ) {
zapperFrame.remove();
zapperFrame = null;
}
sendMessage({ what: 'removeCSS', css: zapperCSS });
zapper.injected = false;
};
/******************************************************************************/
const onFrameMessage = function(msg) {
switch ( msg.what ) {
case 'start':
startZapper();
if ( Boolean(zapperFramePort) === false ) { break; }
highlightElement(true);
break;
case 'quitZapper':
quitZapper();
break;
case 'highlightElementAtPoint':
highlightElementAtPoint(msg.mx, msg.my);
break;
case 'unhighlight':
highlightElement(null);
break;
case 'zapElementAtPoint':
zapElementAtPoint(msg.mx, msg.my, msg.options);
if ( msg.options.highlight !== true && msg.options.stay !== true ) {
quitZapper();
}
break;
default:
break;
}
};
/******************************************************************************/
// zapper-ui.html will be injected in the page through an iframe, and
// is a sandboxed so as to prevent the page from interfering with its
// content and behavior.
//
// The purpose of zapper.js is to install the zapper UI, and wait for the
// component to establish a direct communication channel.
//
// When the zapper is installed on a page, the only change the page sees is an
// iframe with a random attribute. The page can't see the content of the
// iframe, and cannot interfere with its style properties. However the page
// can remove the iframe.
const bootstrap = async ( ) => {
const url = new URL(chrome.runtime.getURL('/zapper-ui.html'));
return new Promise(resolve => {
const frame = document.createElement('iframe');
frame.setAttribute(zapperSecret, '');
document.documentElement.append(frame);
frame.onload = ( ) => {
frame.onload = null;
frame.setAttribute(`${zapperSecret}-loaded`, '');
const channel = new MessageChannel();
const port = channel.port1;
port.onmessage = ev => {
onFrameMessage(ev.data || {});
};
port.onmessageerror = ( ) => {
quitZapper();
};
frame.contentWindow.postMessage(
{ what: 'zapperStart' },
url.origin,
[ channel.port2 ]
);
frame.contentWindow.focus();
resolve({
zapperFrame: frame,
zapperFramePort: port,
});
};
frame.contentWindow.location = url.href;
});
};
let { zapperFrame, zapperFramePort } = await bootstrap();
if ( zapperFrame && zapperFramePort ) { return; }
quitZapper();
/******************************************************************************/
})();
void 0;

View file

@ -23,9 +23,16 @@ import { dom } from './dom.js';
/******************************************************************************/
const mql = self.matchMedia('(prefers-color-scheme: dark)');
const theme = mql instanceof Object && mql.matches === true
? 'dark'
: 'light';
dom.cl.toggle(dom.html, 'dark', theme === 'dark');
dom.cl.toggle(dom.html, 'light', theme !== 'dark');
{
const mql = self.matchMedia('(prefers-color-scheme: dark)');
const theme = mql instanceof Object && mql.matches === true
? 'dark'
: 'light';
dom.cl.toggle(dom.html, 'dark', theme === 'dark');
dom.cl.toggle(dom.html, 'light', theme !== 'dark');
}
{
const mql = self.matchMedia('(hover: hover)');
dom.cl.toggle(dom.html, 'mobile', mql.matches !== true);
}

View file

@ -0,0 +1,226 @@
/*******************************************************************************
uBlock Origin - a comprehensive, efficient content blocker
Copyright (C) 2025-present Raymond Hill
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see {http://www.gnu.org/licenses/}.
Home: https://github.com/gorhill/uBlock
*/
/******************************************************************************/
const $id = id => document.getElementById(id);
const $stor = selector => document.querySelector(selector);
const svgRoot = $stor('svg#sea');
const svgOcean = svgRoot.children[0];
const svgIslands = svgRoot.children[1];
const NoPaths = 'M0 0';
let zapperScriptPort;
/******************************************************************************/
const onSvgClicked = function(ev) {
// If zap mode, highlight element under mouse, this makes the zapper usable
// on touch screens.
zapperScriptPort.postMessage({
what: 'zapElementAtPoint',
mx: ev.clientX,
my: ev.clientY,
options: {
stay: true,
highlight: ev.target !== svgIslands,
},
});
};
/*******************************************************************************
Swipe right:
Remove current highlight
*/
const onSvgTouch = (( ) => {
let startX = 0, startY = 0;
let t0 = 0;
return ev => {
if ( ev.type === 'touchstart' ) {
startX = ev.touches[0].screenX;
startY = ev.touches[0].screenY;
t0 = ev.timeStamp;
return;
}
if ( startX === undefined ) { return; }
const stopX = ev.changedTouches[0].screenX;
const stopY = ev.changedTouches[0].screenY;
const angle = Math.abs(Math.atan2(stopY - startY, stopX - startX));
const distance = Math.sqrt(
Math.pow(stopX - startX, 2) +
Math.pow(stopY - startY, 2)
);
// Interpret touch events as a tap if:
// - Swipe is not valid; and
// - The time between start and stop was less than 200ms.
const duration = ev.timeStamp - t0;
if ( distance < 32 && duration < 200 ) {
onSvgClicked({
type: 'touch',
target: ev.target,
clientX: ev.changedTouches[0].pageX,
clientY: ev.changedTouches[0].pageY,
});
ev.preventDefault();
return;
}
if ( distance < 64 ) { return; }
const angleUpperBound = Math.PI * 0.25 * 0.5;
const swipeRight = angle < angleUpperBound;
if ( swipeRight === false ) { return; }
if ( ev.cancelable ) {
ev.preventDefault();
}
// Swipe right.
if ( svgIslands.getAttribute('d') === NoPaths ) { return; }
zapperScriptPort.postMessage({
what: 'unhighlight'
});
};
})();
/******************************************************************************/
const svgListening = (( ) => {
let on = false;
let timer;
let mx = 0, my = 0;
const onTimer = ( ) => {
timer = undefined;
zapperScriptPort.postMessage({
what: 'highlightElementAtPoint',
mx,
my,
});
};
const onHover = ev => {
mx = ev.clientX;
my = ev.clientY;
if ( timer === undefined ) {
timer = self.requestAnimationFrame(onTimer);
}
};
return state => {
if ( state === on ) { return; }
on = state;
if ( on ) {
document.addEventListener('mousemove', onHover, { passive: true });
return;
}
document.removeEventListener('mousemove', onHover, { passive: true });
if ( timer !== undefined ) {
self.cancelAnimationFrame(timer);
timer = undefined;
}
};
})();
/******************************************************************************/
const onKeyPressed = function(ev) {
// Delete
if ( ev.key === 'Delete' || ev.key === 'Backspace' ) {
zapperScriptPort.postMessage({
what: 'zapElementAtPoint',
options: { stay: true },
});
return;
}
// Esc
if ( ev.key === 'Escape' || ev.which === 27 ) {
quitZapper();
return;
}
};
/******************************************************************************/
const onScriptMessage = function(msg) {
switch ( msg.what ) {
case 'svgPaths': {
let { ocean, islands } = msg;
ocean += islands;
svgOcean.setAttribute('d', ocean);
svgIslands.setAttribute('d', islands || NoPaths);
break;
}
default:
break;
}
};
/******************************************************************************/
const startZapper = function(port) {
zapperScriptPort = port;
zapperScriptPort.onmessage = ev => {
onScriptMessage(ev.data || {});
};
zapperScriptPort.onmessageerror = ( ) => {
quitZapper();
};
zapperScriptPort.postMessage({ what: 'start' });
self.addEventListener('keydown', onKeyPressed, true);
$stor('svg#sea').addEventListener('click', onSvgClicked);
$stor('svg#sea').addEventListener('touchstart', onSvgTouch, { passive: true });
$stor('svg#sea').addEventListener('touchend', onSvgTouch);
$id('quit').addEventListener('click', quitZapper );
svgListening(true);
};
/******************************************************************************/
const quitZapper = function() {
self.removeEventListener('keydown', onKeyPressed, true);
$stor('svg#sea').removeEventListener('click', onSvgClicked);
$stor('svg#sea').removeEventListener('touchstart', onSvgTouch, { passive: true });
$stor('svg#sea').removeEventListener('touchend', onSvgTouch);
$id('quit').removeEventListener('click', quitZapper );
svgListening(false);
if ( zapperScriptPort ) {
zapperScriptPort.postMessage({ what: 'quitZapper' });
zapperScriptPort.close();
zapperScriptPort.onmessage = null;
zapperScriptPort.onmessageerror = null;
zapperScriptPort = null;
}
};
/******************************************************************************/
// Wait for the content script to establish communication
globalThis.addEventListener('message', ev => {
const msg = ev.data || {};
if ( msg.what !== 'zapperStart' ) { return; }
if ( Array.isArray(ev.ports) === false ) { return; }
if ( ev.ports.length === 0 ) { return; }
startZapper(ev.ports[0]);
}, { once: true });
/******************************************************************************/

View file

@ -27,9 +27,7 @@
<!-- -------- -->
<div id="filteringModeText"><label data-i18n="popupFilteringModeLabel">_</label><br><span>_</span><span></span></div>
<div class="toolRibbon pageTools">
<span></span>
<span></span>
<span></span>
<span id="gotoZapper" class="fa-icon tool enabled" data-i18n-title="zapperTipEnter">bolt<span class="caption" data-i18n="zapperTipEnter"></span></span>
<span id="showMatchedRules" class="fa-icon tool" tabindex="0" title="Show matched rules">list-alt<span class="caption">Show matched rules</span></span>
<span id="reportFilterIssue" class="fa-icon tool enabled" data-i18n-title="popupTipReport">comment-alt<span class="caption" data-i18n="popupTipReport"></span></span>
<span id="gotoDashboard" class="fa-icon tool enabled" tabindex="0" data-i18n-title="popupTipDashboard">cogs<span class="caption" data-i18n="popupTipDashboard"></span></span>

View file

@ -0,0 +1,25 @@
<!DOCTYPE html>
<html id="ubol-zapper">
<head>
<meta charset="utf-8">
<meta name="color-scheme" content="light dark">
<title>uBO Lite Zapper</title>
<link rel="stylesheet" href="/css/default.css">
<link rel="stylesheet" href="/css/zapper-ui.css">
</head>
<body>
<aside style="right: 2px; bottom: 2px;">
<div id="quit" data-i18n-title="zapperTipQuit">
<svg viewBox="0 0 64 64"><path d="M16 16L48 48M16 48L48 16" /></svg>
</div>
</aside>
<svg id="sea"><path d></path><path d></path></svg>
<script src="js/theme.js" type="module"></script>
<script src="js/i18n.js" type="module"></script>
<script src="/js/zapper-ui.js" type="module"></script>
</body>
</html>

View file

@ -22,6 +22,11 @@
"strict_min_version": "128.0"
}
},
"commands": {
"enter-zapper-mode": {
"description": "__MSG_zapperTipEnter__"
}
},
"declarative_net_request": {
"rule_resources": [
]
@ -59,6 +64,14 @@
"matches": [
"<all_urls>"
]
},
{
"resources": [
"/zapper-ui.html"
],
"matches": [
"<all_urls>"
]
}
]
}