diff --git a/platform/mv3/chromium/manifest.json b/platform/mv3/chromium/manifest.json index c4d8e96a3..a5ff0306f 100644 --- a/platform/mv3/chromium/manifest.json +++ b/platform/mv3/chromium/manifest.json @@ -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 @@ "" ], "use_dynamic_url": true + }, + { + "resources": [ + "/zapper-ui.html" + ], + "matches": [ + "" + ] } ] } diff --git a/platform/mv3/extension/_locales/en/messages.json b/platform/mv3/extension/_locales/en/messages.json index e4ac1c56c..741e6eb64 100644 --- a/platform/mv3/extension/_locales/en/messages.json +++ b/platform/mv3/extension/_locales/en/messages.json @@ -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" } } diff --git a/platform/mv3/extension/css/popup.css b/platform/mv3/extension/css/popup.css index cc6824674..6ccc8f814 100644 --- a/platform/mv3/extension/css/popup.css +++ b/platform/mv3/extension/css/popup.css @@ -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 { diff --git a/platform/mv3/extension/css/zapper-ui.css b/platform/mv3/extension/css/zapper-ui.css new file mode 100644 index 000000000..cf729ac47 --- /dev/null +++ b/platform/mv3/extension/css/zapper-ui.css @@ -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) +} diff --git a/platform/mv3/extension/js/background.js b/platform/mv3/extension/js/background.js index 7874b3d01..374f771d2 100644 --- a/platform/mv3/extension/js/background.js +++ b/platform/mv3/extension/js/background.js @@ -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(); }); diff --git a/platform/mv3/extension/js/popup.js b/platform/mv3/extension/js/popup.js index d0eefb1bb..a50c889b6 100644 --- a/platform/mv3/extension/js/popup.js +++ b/platform/mv3/extension/js/popup.js @@ -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 || [] ) { diff --git a/platform/mv3/extension/js/scripting/zapper.js b/platform/mv3/extension/js/scripting/zapper.js new file mode 100644 index 000000000..038ea1601 --- /dev/null +++ b/platform/mv3/extension/js/scripting/zapper.js @@ -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; diff --git a/platform/mv3/extension/js/theme.js b/platform/mv3/extension/js/theme.js index bab0fbfd0..a20d459f0 100644 --- a/platform/mv3/extension/js/theme.js +++ b/platform/mv3/extension/js/theme.js @@ -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); +} diff --git a/platform/mv3/extension/js/zapper-ui.js b/platform/mv3/extension/js/zapper-ui.js new file mode 100644 index 000000000..46bf3217a --- /dev/null +++ b/platform/mv3/extension/js/zapper-ui.js @@ -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 }); + +/******************************************************************************/ diff --git a/platform/mv3/extension/popup.html b/platform/mv3/extension/popup.html index fc2307a99..bd1c00d91 100644 --- a/platform/mv3/extension/popup.html +++ b/platform/mv3/extension/popup.html @@ -27,9 +27,7 @@

_
- - - + bolt list-altShow matched rules comment-alt cogs diff --git a/platform/mv3/extension/zapper-ui.html b/platform/mv3/extension/zapper-ui.html new file mode 100644 index 000000000..3e0ef51f8 --- /dev/null +++ b/platform/mv3/extension/zapper-ui.html @@ -0,0 +1,25 @@ + + + + + + +uBO Lite Zapper + + + + + + + + + + + + + + diff --git a/platform/mv3/firefox/manifest.json b/platform/mv3/firefox/manifest.json index 1ecaad2ec..0bd10629a 100644 --- a/platform/mv3/firefox/manifest.json +++ b/platform/mv3/firefox/manifest.json @@ -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": [ "" ] + }, + { + "resources": [ + "/zapper-ui.html" + ], + "matches": [ + "" + ] } ] }