From 5dafbbc59c2a30f8c4992dc1d84df02e90dae0a1 Mon Sep 17 00:00:00 2001 From: Jean Date: Sun, 29 Jun 2025 13:10:54 -0700 Subject: [PATCH] Add theme-and-state-adaptive icons, fixes #29 --- src/background.js | 82 ++++++++++++++++++++++++++++ src/manifest-chrome.json | 60 +++++++++++++++++++++ src/manifest-firefox.json | 93 ++++++++++++++++++++++++++++++++ src/manifest.json | 62 --------------------- src/service-worker-window.html | 8 +++ src/service-worker-window.js | 19 +++++++ src/service-worker.js | 99 ++++++++++++++++++++++++++++++++++ 7 files changed, 361 insertions(+), 62 deletions(-) create mode 100644 src/background.js create mode 100644 src/manifest-chrome.json create mode 100644 src/manifest-firefox.json delete mode 100644 src/manifest.json create mode 100644 src/service-worker-window.html create mode 100644 src/service-worker-window.js create mode 100644 src/service-worker.js diff --git a/src/background.js b/src/background.js new file mode 100644 index 0000000..299cc00 --- /dev/null +++ b/src/background.js @@ -0,0 +1,82 @@ +let colorScheme = "light"; +let state = "neutral"; + +/* Update the extension's icon by color scheme and state */ +function updateIcon(event) { + browser.action.setIcon({ + path: { + 16: "icons/" + colorScheme + "-state-" + state + "-16x.png", + 32: "icons/" + colorScheme + "-state-" + state + "-32x.png", + 48: "icons/" + colorScheme + "-state-" + state + "-48x.png", + 64: "icons/" + colorScheme + "-state-" + state + "-64x.png", + 96: "icons/" + colorScheme + "-state-" + state + "-96x.png", + 128: "icons/" + colorScheme + "-state-" + state + "-128x.png" + }, + }); +} + +/* Listen for color scheme changes */ +window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', updateColorScheme); + +/* Update the color scheme to light or dark */ +function updateColorScheme(event) { + const isDark = event.matches; + if (isDark) { + colorScheme = "dark"; + } else { + colorScheme = "light"; + } + + updateIcon(); +} + +/* Listen for tab changes */ +window.addEventListener("load", updateState()); +browser.tabs.onUpdated.addListener(updateStateOnReload, {urls: ["*://*/*"]}); +browser.tabs.onActivated.addListener(updateState); + +/* Update whether the extension is enabled for a host */ +function updateState() { + let tabs = browser.tabs.query({ active: true, currentWindow: true }); + tabs.then((tabs) => { + const tab = tabs[0]; + const url = new URL(tab.url); + const isAllowedProtocol = url.protocol == "https:" || url.protocol == "http:"; + + if (!isAllowedProtocol) { + state = "neutral"; + updateIcon(); + return; + } + + let storage = browser.storage.local.get("domains"); + storage.then((storage) => { + const hostname = url.host; + const domains = storage.domains; + if (domains === undefined) { + state = "disabled"; + } + else if (domains.includes(hostname)) { + state = "enabled"; + } else { + state = "disabled"; + } + }, logError).then(() => { + updateIcon(); + }, logError); + }, logError); +} + +/* Only update the state if the page was reloaded */ +function updateStateOnReload(tabIs, changeInfo, tab) { + if (changeInfo.url && tab.url.host === changeInfo.url.host) { + return; + } else { + updateState(); + } +} + +/* Required by then */ +function logError(error) { + console.log(`Error: ${error}`); +} diff --git a/src/manifest-chrome.json b/src/manifest-chrome.json new file mode 100644 index 0000000..8b91383 --- /dev/null +++ b/src/manifest-chrome.json @@ -0,0 +1,60 @@ +{ + "manifest_version": 3, + "name": "MetamorPOV", + "author": "Jean Viscogliosi-Pate", + "version": "1.3.0", + + "description": "Enables customization of reader-insert stories by replacing author-provided hooks such as Y/n, pov/s, and vrb/do/present/.", + + "action": { + "default_title": "MetamorPOV", + "default_popup": "popup.html", + "default_icon": { + "16": "icons/light-state-neutral-16x.png", + "32": "icons/light-state-neutral-32x.png", + "48": "icons/light-state-neutral-48x.png", + "64": "icons/light-state-neutral-64x.png", + "96": "icons/light-state-neutral-96x.png", + "128": "icons/light-state-neutral-128x.png" + } + }, + + "permissions": [ + "storage", + "tabs", + "offscreen" + ], + + "host_permissions": [ + "http://*/*", + "https://*/*" + ], + + "icons": { + "16": "icons/light-state-neutral-16x.png", + "32": "icons/light-state-neutral-32x.png", + "48": "icons/light-state-neutral-48x.png", + "64": "icons/light-state-neutral-64x.png", + "96": "icons/light-state-neutral-96x.png", + "128": "icons/light-state-neutral-128x.png" + }, + + "content_scripts": [ + { + "js": [ + "content-script.js" + ], + "run_at": "document_idle", + "matches": [ + "*://*/*" + ] + } + ], + + "background": { + "service_worker": "service-worker.js", + "type": "module" + }, + + "homepage_url": "https://git.viscogliosi-pate.com/jean/metamorpov" +} diff --git a/src/manifest-firefox.json b/src/manifest-firefox.json new file mode 100644 index 0000000..4a8262d --- /dev/null +++ b/src/manifest-firefox.json @@ -0,0 +1,93 @@ +{ + "manifest_version": 3, + "name": "MetamorPOV", + "author": "Jean Viscogliosi-Pate", + "version": "1.3.0", + + "description": "Enables customization of reader-insert stories by replacing author-provided hooks such as Y/n, pov/s, and vrb/do/present/.", + + "browser_specific_settings": { + "gecko": { + "id": "metamorpov@viscogliosi-pate.com", + "strict_min_version": "113.0" + }, + "gecko_android": {} + }, + + "action": { + "default_title": "MetamorPOV", + "default_popup": "popup.html", + "default_icon": { + "16": "icons/light-state-neutral-16x.png", + "32": "icons/light-state-neutral-32x.png", + "48": "icons/light-state-neutral-48x.png", + "64": "icons/light-state-neutral-64x.png", + "96": "icons/light-state-neutral-96x.png", + "128": "icons/light-state-neutral-128x.png" + }, + "theme_icons": [{ + "light": "icons/light-state-neutral-16x.png", + "dark": "icons/dark-state-neutral-16x.png", + "size": 16 + }, { + "light": "icons/light-state-neutral-32x.png", + "dark": "icons/dark-state-neutral-32x.png", + "size": 32 + }, { + "light": "icons/light-state-neutral-48x.png", + "dark": "icons/dark-state-neutral-48x.png", + "size": 48 + }, { + "light": "icons/light-state-neutral-64x.png", + "dark": "icons/dark-state-neutral-64x.png", + "size": 64 + }, { + "light": "icons/light-state-neutral-96x.png", + "dark": "icons/dark-state-neutral-96x.png", + "size": 96 + }, { + "light": "icons/light-state-neutral-128x.png", + "dark": "icons/dark-state-neutral-128x.png", + "size": 128 + }] + }, + + "permissions": [ + "storage", + "tabs" + ], + + "host_permissions": [ + "http://*/*", + "https://*/*" + ], + + "icons": { + "16": "icons/light-state-neutral-16x.png", + "32": "icons/light-state-neutral-32x.png", + "48": "icons/light-state-neutral-48x.png", + "64": "icons/light-state-neutral-64x.png", + "96": "icons/light-state-neutral-96x.png", + "128": "icons/light-state-neutral-128x.png" + }, + + "content_scripts": [ + { + "js": [ + "content-script.js" + ], + "run_at": "document_idle", + "matches": [ + "*://*/*" + ] + } + ], + + "background": { + "scripts": [ + "background.js" + ] + }, + + "homepage_url": "https://git.viscogliosi-pate.com/jean/metamorpov" +} diff --git a/src/manifest.json b/src/manifest.json deleted file mode 100644 index 4c4aaf3..0000000 --- a/src/manifest.json +++ /dev/null @@ -1,62 +0,0 @@ -{ - "manifest_version": 3, - "name": "MetamorPOV", - "author": "Jean Viscogliosi-Pate", - "version": "1.2.0", - - "description": "Enables customization of reader-insert stories by replacing author-provided hooks such as Y/n, pov/s, and vrb/do/present/.", - - "browser_specific_settings": { - "gecko": { - "id": "metamorpov@viscogliosi-pate.com", - "strict_min_version": "113.0" - }, - "gecko_android": {} - }, - - "action": { - "default_title": "MetamorPOV", - "default_icon": { - "16": "icons/16x.png", - "32": "icons/32x.png", - "48": "icons/48x.png", - "64": "icons/64x.png", - "96": "icons/96x.png", - "128": "icons/128x.png" - }, - "default_popup": "popup.html" - }, - - "permissions": [ - "storage", - "tabs" - ], - - "host_permissions": [ - "http://*/*", - "https://*/*" - ], - - "icons": { - "16": "icons/16x.png", - "32": "icons/32x.png", - "48": "icons/48x.png", - "64": "icons/64x.png", - "96": "icons/96x.png", - "128": "icons/128x.png" - }, - - "content_scripts": [ - { - "js": [ - "content-script.js" - ], - "run_at": "document_idle", - "matches": [ - "*://*/*" - ] - } - ], - - "homepage_url": "https://git.viscogliosi-pate.com/jean/metamorpov" -} diff --git a/src/service-worker-window.html b/src/service-worker-window.html new file mode 100644 index 0000000..57e1edd --- /dev/null +++ b/src/service-worker-window.html @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/src/service-worker-window.js b/src/service-worker-window.js new file mode 100644 index 0000000..d19719b --- /dev/null +++ b/src/service-worker-window.js @@ -0,0 +1,19 @@ +/* Broadcast color scheme on browser start, because no change event is fired */ +updateColorScheme(window.matchMedia('(prefers-color-scheme: dark)')); + +function updateColorScheme(event) { + console.log("doing!"); + chrome.runtime.sendMessage({ + content: "colorScheme", + isDark: event.matches + }); +} + +/* Broadcast when page is loaded */ +window.addEventListener("load", updateState); + +function updateState() { + chrome.runtime.sendMessage({ + content: "updateState" + }); +} diff --git a/src/service-worker.js b/src/service-worker.js new file mode 100644 index 0000000..63b3ddf --- /dev/null +++ b/src/service-worker.js @@ -0,0 +1,99 @@ +let colorScheme = "light"; +let state = "neutral"; + +/* Update the extension's icon by color scheme and state */ +function updateIcon(event) { + chrome.action.setIcon({ + path: { + 16: "icons/" + colorScheme + "-state-" + state + "-16x.png", + 32: "icons/" + colorScheme + "-state-" + state + "-32x.png", + 48: "icons/" + colorScheme + "-state-" + state + "-48x.png", + 64: "icons/" + colorScheme + "-state-" + state + "-64x.png", + 96: "icons/" + colorScheme + "-state-" + state + "-96x.png", + 128: "icons/" + colorScheme + "-state-" + state + "-128x.png" + }, + }); +} + +/* Track CSS media feature offscreen */ +chrome.offscreen.createDocument({ + url: "service-worker-window.html", + reasons: ["MATCH_MEDIA"], + justification: "Update icon by color scheme", +}) + +/* Listen for color scheme changes */ +chrome.runtime.onMessage.addListener( + function(request, sender, sendResponse) { + if (request.content === "colorScheme") { + updateColorScheme(request.isDark); + } + } +) + +/* Update the color scheme to light or dark */ +function updateColorScheme(isDark) { + if (isDark) { + colorScheme = "dark"; + } else { + colorScheme = "light"; + } + + updateIcon(); +} + +/* Listen for tab changes */ +chrome.tabs.onUpdated.addListener(updateStateOnReload); +chrome.tabs.onActivated.addListener(updateState); + +/* Update whether the extension is enabled for a host */ +function updateState() { + let tabs = chrome.tabs.query({ active: true, currentWindow: true }); + tabs.then((tabs) => { + const tab = tabs[0]; + if (tab.url === "") { + state = "neutral"; + updateIcon(); + return; + } + + const url = new URL(tab.url); + const isAllowedProtocol = url.protocol == "https:" || url.protocol == "http:"; + + if (!isAllowedProtocol) { + state = "neutral"; + updateIcon(); + return; + } + + let storage = chrome.storage.local.get("domains"); + storage.then((storage) => { + const hostname = url.host; + const domains = storage.domains; + if (domains === undefined) { + state = "disabled"; + } + else if (domains.includes(hostname)) { + state = "enabled"; + } else { + state = "disabled"; + } + }, logError).then(() => { + updateIcon(); + }, logError); + }, logError); +} + +/* Only update the state if the page was reloaded */ +function updateStateOnReload(tabIs, changeInfo, tab) { + if (changeInfo.url && tab.url.host === changeInfo.url.host) { + return; + } else { + updateState(); + } +} + +/* Required by then */ +function logError(error) { + console.log(`Error: ${error}`); +}