From 7e64a428ac438a94d9b05c4bb8642520fc13028c Mon Sep 17 00:00:00 2001 From: Jordan Wages Date: Tue, 8 Jul 2025 01:30:37 -0500 Subject: [PATCH] Add theme support with dynamic icons --- README.md | 1 + background.js | 113 ++++++++++++++++++++++++------------------- details.js | 6 +++ options/options.html | 39 ++++++++++----- options/options.js | 38 ++++++++++++++- 5 files changed, 132 insertions(+), 65 deletions(-) diff --git a/README.md b/README.md index bdff14a..4447c9b 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,7 @@ message meets a specified criterion. - **Advanced parameters** – tune generation settings like temperature, top‑p and more from the options page. - **Markdown conversion** – optionally convert HTML bodies to Markdown before sending them to the AI service. - **Debug logging** – optional colorized logs help troubleshoot interactions with the AI service. +- **Light/Dark themes** – automatically match Thunderbird's appearance with optional manual override. - **Automatic rules** – create rules that tag or move new messages based on AI classification. - **Rule ordering** – drag rules to prioritize them and optionally stop processing after a match. - **Context menu** – apply AI rules from the message list or the message display action button. diff --git a/background.js b/background.js index 2916c06..fcccc6b 100644 --- a/background.js +++ b/background.js @@ -27,24 +27,41 @@ let stripUrlParams = false; let altTextImages = false; let collapseWhitespace = false; let TurndownService = null; +let userTheme = 'auto'; +let currentTheme = 'light'; + +function iconPaths(name) { + return { + 16: `resources/img/${name}-${currentTheme}-16.png`, + 32: `resources/img/${name}-${currentTheme}-32.png`, + 64: `resources/img/${name}-${currentTheme}-64.png` + }; +} + +async function detectSystemTheme() { + try { + const t = await browser.theme.getCurrent(); + const scheme = t?.properties?.color_scheme; + if (scheme === 'dark' || scheme === 'light') { + return scheme; + } + const color = t?.colors?.frame || t?.colors?.toolbar; + if (color && /^#/.test(color)) { + const r = parseInt(color.slice(1, 3), 16); + const g = parseInt(color.slice(3, 5), 16); + const b = parseInt(color.slice(5, 7), 16); + const lum = (0.299 * r + 0.587 * g + 0.114 * b) / 255; + return lum < 0.5 ? 'dark' : 'light'; + } + } catch {} + return 'light'; +} const ICONS = { - logo: "resources/img/logo.png", - circledots: { - 16: "resources/img/circledots-16.png", - 32: "resources/img/circledots-32.png", - 64: "resources/img/circledots-64.png" - }, - circle: { - 16: "resources/img/circle-16.png", - 32: "resources/img/circle-32.png", - 64: "resources/img/circle-64.png" - }, - average: { - 16: "resources/img/average-16.png", - 32: "resources/img/average-32.png", - 64: "resources/img/average-64.png" - } + logo: () => 'resources/img/logo.png', + circledots: () => iconPaths('circledots'), + circle: () => iconPaths('circle'), + average: () => iconPaths('average') }; function setIcon(path) { @@ -57,19 +74,29 @@ function setIcon(path) { } function updateActionIcon() { - let path = ICONS.logo; + let path = ICONS.logo(); if (processing || queuedCount > 0) { - path = ICONS.circledots; + path = ICONS.circledots(); } setIcon(path); } -function showTransientIcon(path, delay = 1500) { +function showTransientIcon(factory, delay = 1500) { clearTimeout(iconTimer); + const path = typeof factory === 'function' ? factory() : factory; setIcon(path); iconTimer = setTimeout(updateActionIcon, delay); } +function refreshMenuIcons() { + browser.menus.update('apply-ai-rules-list', { icons: iconPaths('eye') }); + browser.menus.update('apply-ai-rules-display', { icons: iconPaths('eye') }); + browser.menus.update('clear-ai-cache-list', { icons: iconPaths('trash') }); + browser.menus.update('clear-ai-cache-display', { icons: iconPaths('trash') }); + browser.menus.update('view-ai-reason-list', { icons: iconPaths('clipboarddata') }); + browser.menus.update('view-ai-reason-display', { icons: iconPaths('clipboarddata') }); +} + function byteSize(str) { return new TextEncoder().encode(str || "").length; @@ -286,9 +313,11 @@ async function clearCacheForMessages(idsInput) { } try { - const store = await storage.local.get(["endpoint", "templateName", "customTemplate", "customSystemPrompt", "aiParams", "debugLogging", "htmlToMarkdown", "stripUrlParams", "altTextImages", "collapseWhitespace", "aiRules"]); + const store = await storage.local.get(["endpoint", "templateName", "customTemplate", "customSystemPrompt", "aiParams", "debugLogging", "htmlToMarkdown", "stripUrlParams", "altTextImages", "collapseWhitespace", "aiRules", "theme"]); logger.setDebug(store.debugLogging); await AiClassifier.setConfig(store); + userTheme = store.theme || 'auto'; + currentTheme = userTheme === 'auto' ? await detectSystemTheme() : userTheme; await AiClassifier.init(); htmlToMarkdown = store.htmlToMarkdown === true; stripUrlParams = store.stripUrlParams === true; @@ -341,12 +370,19 @@ async function clearCacheForMessages(idsInput) { collapseWhitespace = changes.collapseWhitespace.newValue === true; logger.aiLog("collapseWhitespace updated from storage change", { debug: true }, collapseWhitespace); } + if (changes.theme) { + userTheme = changes.theme.newValue || 'auto'; + currentTheme = userTheme === 'auto' ? await detectSystemTheme() : userTheme; + updateActionIcon(); + refreshMenuIcons(); + } }); } catch (err) { logger.aiLog("failed to load config", { level: 'error' }, err); } logger.aiLog("background.js loaded – ready to classify", { debug: true }); + updateActionIcon(); if (browser.messageDisplayAction) { browser.messageDisplayAction.setTitle({ title: "Details" }); if (browser.messageDisplayAction.setLabel) { @@ -359,62 +395,39 @@ async function clearCacheForMessages(idsInput) { id: "apply-ai-rules-list", title: "Apply AI Rules", contexts: ["message_list"], - icons: { - 16: "resources/img/eye-16.png", - 32: "resources/img/eye-32.png", - 64: "resources/img/eye-64.png" - } + icons: iconPaths('eye') }); browser.menus.create({ id: "apply-ai-rules-display", title: "Apply AI Rules", contexts: ["message_display_action"], - icons: { - 16: "resources/img/eye-16.png", - 32: "resources/img/eye-32.png", - 64: "resources/img/eye-64.png" - } + icons: iconPaths('eye') }); browser.menus.create({ id: "clear-ai-cache-list", title: "Clear AI Cache", contexts: ["message_list"], - icons: { - 16: "resources/img/trash-16.png", - 32: "resources/img/trash-32.png", - 64: "resources/img/trash-64.png" - } + icons: iconPaths('trash') }); browser.menus.create({ id: "clear-ai-cache-display", title: "Clear AI Cache", contexts: ["message_display_action"], - icons: { - 16: "resources/img/trash-16.png", - 32: "resources/img/trash-32.png", - 64: "resources/img/trash-64.png" - } + icons: iconPaths('trash') }); browser.menus.create({ id: "view-ai-reason-list", title: "View Reasoning", contexts: ["message_list"], - icons: { - 16: "resources/img/clipboarddata-16.png", - 32: "resources/img/clipboarddata-32.png", - 64: "resources/img/clipboarddata-64.png" - } + icons: iconPaths('clipboarddata') }); browser.menus.create({ id: "view-ai-reason-display", title: "View Reasoning", contexts: ["message_display_action"], - icons: { - 16: "resources/img/clipboarddata-16.png", - 32: "resources/img/clipboarddata-32.png", - 64: "resources/img/clipboarddata-64.png" - } + icons: iconPaths('clipboarddata') }); + refreshMenuIcons(); browser.menus.onClicked.addListener(async (info, tab) => { if (info.menuItemId === "apply-ai-rules-list" || info.menuItemId === "apply-ai-rules-display") { diff --git a/details.js b/details.js index f84ebf4..cad1979 100644 --- a/details.js +++ b/details.js @@ -1,4 +1,10 @@ const aiLog = (await import(browser.runtime.getURL("logger.js"))).aiLog; +const storage = (globalThis.messenger ?? browser).storage; +const { theme } = await storage.local.get('theme'); +const mode = (theme || 'auto') === 'auto' + ? (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light') + : theme; +document.documentElement.dataset.theme = mode; const qMid = parseInt(new URLSearchParams(location.search).get("mid"), 10); if (!isNaN(qMid)) { diff --git a/options/options.html b/options/options.html index 0fc5cae..58cfe37 100644 --- a/options/options.html +++ b/options/options.html @@ -37,22 +37,22 @@
- AI Filter Logo + AI Filter Logo
@@ -60,7 +60,7 @@

- + Settings

@@ -94,13 +94,26 @@
+
+ +
+
+ +
+
+
+
@@ -202,7 +215,7 @@