diff --git a/README.md b/README.md index d2cb49a..3c17b8b 100644 --- a/README.md +++ b/README.md @@ -34,30 +34,29 @@ To run this extension from the repo (and not from a pre-built package) on a loca ## Browser Support -| Web Browser | Platform | Web Push Notification Support | Background Notification Support (When Browser Closed) | -|---------------------|----------|-------------------------------|-------------------------------------------------------| -| **Google Chrome** | macOS | ✅ Yes | ❌ No (unless running in the background) | -| | Windows | ✅ Yes | ❌ No (unless running in the background) | -| | iOS | ❌ No | ❌ No | -| | Android | ✅ Yes | ✅ Yes | -| **Safari** | macOS | ✅ Yes | ✅ Yes | -| | iOS | ✅ Yes | ✅ Yes (site must be added to Home Screen first) | -| **Microsoft Edge** | macOS | ✅ Yes | ❌ No (unless running in the background) | -| | Windows | ✅ Yes | ❌ No (unless running in the background) | -| | iOS | ❌ No | ❌ No | -| | Android | ✅ Yes | ✅ Yes | -| **Mozilla Firefox** | macOS | ✅ Yes | ❌ No | -| | Windows | ✅ Yes | ❌ No | -| | iOS | ❌ No | ❌ No | -| | Android | ✅ Yes | ❌ No | -| **Opera** | macOS | ✅ Yes | ❌ No | -| | Windows | ✅ Yes | ❌ No | -| | iOS | ❌ No | ❌ No | -| | Android | ✅ Yes | ❌ No | - -*(unless running in the background)* means the browsers have background processes running (they’re not fully quit). - -More info here https://caniuse.com/push-api +The table covers current stable browsers using the standards-based [Push API](https://w3c.github.io/push-api/), [Notifications API](https://notifications.spec.whatwg.org/), and [Service Workers](https://w3c.github.io/ServiceWorker/). “Background delivery” means delivery when the site is not open. A fully quit desktop browser is called out separately. Last reviewed September 2026. + +| Web browser | Platform | Web Push support | Background delivery | +|-------------------------|---------------------------------------|-----------------------------------------------|------------------------------------------------------------------------------| +| **Google Chrome** | Windows, macOS, Linux | ✅ Yes | ⚠️ Only while Chrome or its background process is running | +| | ChromeOS | ✅ Yes | ✅ Yes, while the device is running | +| | Android | ✅ Yes | ✅ Yes, unless the app is force-stopped or background activity is restricted | +| **Safari** | macOS Ventura or later (Safari 16.1+) | ✅ Yes | ✅ Yes, even when Safari is not running | +| **Microsoft Edge** | Windows, macOS, Linux | ✅ Yes | ✅ Yes, subject to browser and OS background settings | +| | Android | ✅ Yes | ✅ Yes, unless the app is force-stopped or background activity is restricted | +| **Mozilla Firefox** | Windows, macOS, Linux | ✅ Yes | ❌ No after Firefox is fully quit | +| | Android | ✅ Yes | ✅ Yes, unless the app is force-stopped or background activity is restricted | +| **Opera** | Windows, macOS, Linux | ✅ Yes | ⚠️ Only while Opera or its background process is running | +| | Android | ✅ Yes | ✅ Yes, unless the app is force-stopped or background activity is restricted | +| **Samsung Internet** | Android | ✅ Yes | ✅ Yes, unless the app is force-stopped or background activity is restricted | +| **Home Screen web app** | iOS/iPadOS 16.4+ | ✅ Yes, after installation to the Home Screen | ✅ Yes | +| **Opera Mini** | Mobile | ❌ No | ❌ No | + +On iOS and iPadOS, Web Push is not available to a normal browser tab. It belongs to an installed Home Screen web app and works regardless of which browser added that app to the Home Screen. The extension provides the required `standalone` web app manifest. Notification permission must be requested from a direct user interaction. + +All supported platforms require user permission and a secure context (`https://`, with `http://localhost` allowed for development). Private browsing, enterprise policy, notification settings, Focus/Do Not Disturb modes, power-saving features, and OS background restrictions can prevent delivery. + +References: [Push API compatibility](https://caniuse.com/push-api), [Push API](https://developer.mozilla.org/en-US/docs/Web/API/Push_API), [Web Push on iOS and iPadOS](https://webkit.org/blog/13878/web-push-for-web-apps-on-ios-and-ipados/), [Web Push on macOS](https://webkit.org/blog/12945/meet-web-push/), [Edge website notifications](https://support.microsoft.com/en-us/edge/manage-website-notifications-in-microsoft-edge), and [Firefox Web Push notifications](https://support.mozilla.org/en-US/kb/push-notifications-firefox). ## Testing Push Notifications diff --git a/adm/style/pwa_acp.css b/adm/style/pwa_acp.css index 961f0ef..d6209f4 100644 --- a/adm/style/pwa_acp.css +++ b/adm/style/pwa_acp.css @@ -27,3 +27,13 @@ input[type="color"]::-moz-color-swatch { display: inline-block; width: 120px; } + +.pwa-colour-preview { + position: fixed; + left: -10000px; + top: 0; + visibility: hidden; + width: 1280px; + height: 800px; + border: 0; +} diff --git a/adm/style/pwa_acp.js b/adm/style/pwa_acp.js index 8705500..1237da6 100644 --- a/adm/style/pwa_acp.js +++ b/adm/style/pwa_acp.js @@ -2,6 +2,7 @@ document.addEventListener('DOMContentLoaded', () => { 'use strict'; const HEX_REGEX = /^#([A-Fa-f0-9]{6})$/; + const DETECTION_TIMEOUT = 30000; const colorPickers = document.querySelectorAll('input[type="color"]'); @@ -35,4 +36,116 @@ document.addEventListener('DOMContentLoaded', () => { syncColors(colorText, colorPicker); }); + + // Read an element's computed background colour as RGBA values. + const readColor = (view, element) => { + const value = view.getComputedStyle(element).backgroundColor; + const canvas = document.createElement('canvas'); + const context = canvas.getContext('2d'); + + canvas.width = 1; + canvas.height = 1; + context.clearRect(0, 0, 1, 1); + context.fillStyle = value; + context.fillRect(0, 0, 1, 1); + + return Array.from(context.getImageData(0, 0, 1, 1).data); + }; + + // Blend a foreground colour over a background colour. + const compositeColor = (foreground, background) => { + const alpha = foreground[3] / 255; + + return [ + Math.round(foreground[0] * alpha + background[0] * (1 - alpha)), + Math.round(foreground[1] * alpha + background[1] * (1 - alpha)), + Math.round(foreground[2] * alpha + background[2] * (1 - alpha)), + 255, + ]; + }; + + // Resolve the visible page colour from the preview's HTML and body backgrounds. + const detectPageColor = iframe => { + const previewDocument = iframe.contentDocument; + const previewWindow = iframe.contentWindow; + const htmlColor = readColor(previewWindow, previewDocument.documentElement); + const bodyColor = readColor(previewWindow, previewDocument.body); + + if (htmlColor[3] === 0 && bodyColor[3] === 0) { + return null; + } + + const canvasColor = compositeColor(htmlColor, [ 255, 255, 255, 255 ]); + return compositeColor(bodyColor, canvasColor); + }; + + // Convert RGB values to a six-digit hexadecimal colour. + const toHex = color => '#' + color.slice(0, 3) + .map(channel => channel.toString(16).padStart(2, '0')) + .join(''); + + // Update a style's text field and synchronize its colour picker. + const setColor = (styleId, name, value) => { + const colorText = document.getElementById(`pwa_${name}_color_${styleId}`); + + colorText.value = value; + colorText.dispatchEvent(new Event('input', { bubbles: true })); + }; + + // Attach colour detection to each installed style's button. + document.querySelectorAll('.pwa-detect-colours').forEach(button => { + button.addEventListener('click', () => { + const originalLabel = button.value; + const status = button.closest('dd').querySelector('.pwa-detect-status'); + const iframe = document.createElement('iframe'); + const previewUrl = new URL(button.dataset.previewUrl, window.location.href); + let finished = false; + + // Remove the preview and restore the detector button after completion. + const finish = error => { + if (finished) { + return; + } + + finished = true; + clearTimeout(timeout); + iframe.remove(); + button.disabled = false; + button.value = originalLabel; + status.textContent = error ? button.dataset.errorMessage : ''; + }; + + const timeout = setTimeout(() => finish(true), DETECTION_TIMEOUT); + + button.disabled = true; + button.value = button.dataset.detectingLabel; + status.textContent = ''; + status.title = ''; + iframe.className = 'pwa-colour-preview'; + iframe.setAttribute('aria-hidden', 'true'); + iframe.setAttribute('sandbox', 'allow-same-origin'); + iframe.addEventListener('load', () => { + try { + const themeColor = detectPageColor(iframe); + + if (!themeColor) { + throw new Error(button.dataset.noBackgroundMessage); + } + + const pageColor = toHex(themeColor); + + setColor(button.dataset.styleId, 'theme', pageColor); + setColor(button.dataset.styleId, 'bg', pageColor); + finish(false); + } catch (error) { + status.title = error.message; + finish(true); + } + }); + iframe.addEventListener('error', () => finish(true)); + previewUrl.searchParams.set('style', button.dataset.styleId); + iframe.src = previewUrl.href; + document.body.appendChild(iframe); + }); + }); }); diff --git a/adm/style/wpn_acp_pwa.html b/adm/style/wpn_acp_pwa.html index d9b74a4..ce4e063 100644 --- a/adm/style/wpn_acp_pwa.html +++ b/adm/style/wpn_acp_pwa.html @@ -60,6 +60,16 @@