From 13751b3ab2741b23cbbf5fc0e8e497ed7dea203c Mon Sep 17 00:00:00 2001 From: Jordan Wages Date: Sun, 6 Jul 2025 18:05:19 -0500 Subject: [PATCH 01/57] Trying new things --- background.js | 24 ++++++++++++++---------- details.js | 14 +++++++------- manifest.json | 6 ++---- 3 files changed, 23 insertions(+), 21 deletions(-) diff --git a/background.js b/background.js index d5a5e3a..b600169 100644 --- a/background.js +++ b/background.js @@ -258,7 +258,7 @@ async function clearCacheForMessages(idsInput) { logger = await import(browser.runtime.getURL("logger.js")); try { AiClassifier = await import(browser.runtime.getURL("modules/AiClassifier.js")); - logger.aiLog("AiClassifier imported", {debug: true}); + logger.aiLog("AiClassifier imported", { debug: true }); const td = await import(browser.runtime.getURL("resources/js/turndown.js")); TurndownService = td.default || td.TurndownService; } catch (e) { @@ -291,7 +291,7 @@ async function clearCacheForMessages(idsInput) { if (r.stopProcessing) rule.stopProcessing = true; return rule; }) : []; - logger.aiLog("configuration loaded", {debug: true}, store); + logger.aiLog("configuration loaded", { debug: true }, store); storage.onChanged.addListener(async changes => { if (changes.aiRules) { const newRules = changes.aiRules.newValue || []; @@ -304,30 +304,30 @@ async function clearCacheForMessages(idsInput) { if (r.stopProcessing) rule.stopProcessing = true; return rule; }); - logger.aiLog("aiRules updated from storage change", {debug: true}, aiRules); + logger.aiLog("aiRules updated from storage change", { debug: true }, aiRules); } if (changes.htmlToMarkdown) { htmlToMarkdown = changes.htmlToMarkdown.newValue === true; - logger.aiLog("htmlToMarkdown updated from storage change", {debug: true}, htmlToMarkdown); + logger.aiLog("htmlToMarkdown updated from storage change", { debug: true }, htmlToMarkdown); } if (changes.stripUrlParams) { stripUrlParams = changes.stripUrlParams.newValue === true; - logger.aiLog("stripUrlParams updated from storage change", {debug: true}, stripUrlParams); + logger.aiLog("stripUrlParams updated from storage change", { debug: true }, stripUrlParams); } if (changes.altTextImages) { altTextImages = changes.altTextImages.newValue === true; - logger.aiLog("altTextImages updated from storage change", {debug: true}, altTextImages); + logger.aiLog("altTextImages updated from storage change", { debug: true }, altTextImages); } if (changes.collapseWhitespace) { collapseWhitespace = changes.collapseWhitespace.newValue === true; - logger.aiLog("collapseWhitespace updated from storage change", {debug: true}, collapseWhitespace); + logger.aiLog("collapseWhitespace updated from storage change", { debug: true }, collapseWhitespace); } }); } catch (err) { - logger.aiLog("failed to load config", {level: 'error'}, err); + logger.aiLog("failed to load config", { level: 'error' }, err); } - logger.aiLog("background.js loaded – ready to classify", {debug: true}); + logger.aiLog("background.js loaded – ready to classify", { debug: true }); if (browser.messageDisplayAction) { browser.messageDisplayAction.setTitle({ title: "Details" }); if (browser.messageDisplayAction.setLabel) { @@ -372,7 +372,7 @@ async function clearCacheForMessages(idsInput) { //for the love of god work please browser.messageDisplayAction.onClicked.addListener(async (tab, info) => { try { - let header = await browser.messageDisplay.getDisplayedMessages(); + let header = await browser.messageDisplay.getDisplayedMessages(tab.id); if (!header) { logger.aiLog("No header, no message loaded?", { debug: true }); return; @@ -386,6 +386,10 @@ async function clearCacheForMessages(idsInput) { } }); + browser.messageDisplay.onMessagesDisplayed.addListener(async (tab, displayedMessages) => { + logger.aiLog("Messages displayed!", { debug: true }, displayedMessages); + }); + browser.menus.onClicked.addListener(async (info, tab) => { if (info.menuItemId === "apply-ai-rules-list" || info.menuItemId === "apply-ai-rules-display") { const ids = info.selectedMessages?.messages?.map(m => m.id) || diff --git a/details.js b/details.js index 016783a..6269dae 100644 --- a/details.js +++ b/details.js @@ -8,14 +8,14 @@ document.addEventListener("DOMContentLoaded", async () => { } const thisTab = await browser.tabs.getCurrent(); - const baseTabId = thisTab.openerTabId ?? thisTab.id; - const [header] = await browser.messageDisplay.getDisplayedMessages(baseTabId); + //const baseTabId = thisTab.openerTabId ?? thisTab.id; + //const [header] = await browser.messageDisplay.getDisplayedMessages(baseTabId); - if (header) { - loadMessage(header.id); - } else { - aiLog("Details popup: no displayed message found"); - } + //if (header) { + // loadMessage(header.id); + //} else { + // aiLog("Details popup: no displayed message found"); + //} }); async function loadMessage(id) { diff --git a/manifest.json b/manifest.json index 0d6db48..fbd37ae 100644 --- a/manifest.json +++ b/manifest.json @@ -40,8 +40,6 @@ "accountsRead", "menus", "scripting", - "tabs", - "*://*/*" - ], - "content_security_policy": "script-src 'self'; object-src 'none'; connect-src 'self' http: https:" + "tabs" + ] } From caf18ed5ab1c5db206303b2a79f0578538c998ce Mon Sep 17 00:00:00 2001 From: Jordan Wages Date: Mon, 7 Jul 2025 00:14:47 -0500 Subject: [PATCH 02/57] Add message for displayed messages and convert details to module --- background.js | 9 +++++++++ details.html | 2 +- details.js | 15 ++++++--------- 3 files changed, 16 insertions(+), 10 deletions(-) diff --git a/background.js b/background.js index b600169..d58adf7 100644 --- a/background.js +++ b/background.js @@ -501,6 +501,15 @@ async function clearCacheForMessages(idsInput) { logger.aiLog("failed to collect details", { level: 'error' }, e); return { subject: '', results: [] }; } + } else if (msg?.type === "sortana:getDisplayedMessages") { + try { + const [tab] = await browser.tabs.query({ active: true, currentWindow: true }); + const messages = await browser.messageDisplay.getDisplayedMessages(tab?.id); + return { messages }; + } catch (e) { + logger.aiLog("failed to get displayed messages", { level: 'error' }, e); + return { messages: [] }; + } } else if (msg?.type === "sortana:clearCacheForMessage") { try { await clearCacheForMessages([msg.id]); diff --git a/details.html b/details.html index 1502471..d15a3c9 100644 --- a/details.html +++ b/details.html @@ -15,6 +15,6 @@ - + diff --git a/details.js b/details.js index 6269dae..ca53cbe 100644 --- a/details.js +++ b/details.js @@ -1,12 +1,9 @@ -document.addEventListener("DOMContentLoaded", async () => { - const aiLog = (await import(browser.runtime.getURL("logger.js"))).aiLog; - - const qMid = parseInt(new URLSearchParams(location.search).get("mid"), 10); - if (!isNaN(qMid)) { - loadMessage(qMid); - return; - } +const aiLog = (await import(browser.runtime.getURL("logger.js"))).aiLog; +const qMid = parseInt(new URLSearchParams(location.search).get("mid"), 10); +if (!isNaN(qMid)) { + loadMessage(qMid); +} else { const thisTab = await browser.tabs.getCurrent(); //const baseTabId = thisTab.openerTabId ?? thisTab.id; //const [header] = await browser.messageDisplay.getDisplayedMessages(baseTabId); @@ -16,7 +13,7 @@ document.addEventListener("DOMContentLoaded", async () => { //} else { // aiLog("Details popup: no displayed message found"); //} -}); +} async function loadMessage(id) { const storage = (globalThis.messenger ?? browser).storage; From 51816d8a19d7c8976ada797b5116c711700b97b2 Mon Sep 17 00:00:00 2001 From: Jordan Wages Date: Mon, 7 Jul 2025 00:23:54 -0500 Subject: [PATCH 03/57] Use getDisplayedMessages to load message --- details.js | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/details.js b/details.js index ca53cbe..1e9023f 100644 --- a/details.js +++ b/details.js @@ -4,15 +4,14 @@ const qMid = parseInt(new URLSearchParams(location.search).get("mid"), 10); if (!isNaN(qMid)) { loadMessage(qMid); } else { - const thisTab = await browser.tabs.getCurrent(); - //const baseTabId = thisTab.openerTabId ?? thisTab.id; - //const [header] = await browser.messageDisplay.getDisplayedMessages(baseTabId); - - //if (header) { - // loadMessage(header.id); - //} else { - // aiLog("Details popup: no displayed message found"); - //} + const { messages } = await browser.runtime.sendMessage({ + type: "sortana:getDisplayedMessages", + }); + if (messages && messages[0]) { + loadMessage(messages[0].id); + } else { + aiLog("Details popup: no displayed message found"); + } } async function loadMessage(id) { From 6a85dbb2eb12ff072871c4aa429e0bdf89b1f310 Mon Sep 17 00:00:00 2001 From: Jordan Wages Date: Mon, 7 Jul 2025 21:46:21 -0500 Subject: [PATCH 04/57] Going back to what works. Manifest v3 and its consequences have been a disaster for the human race. --- background.js | 283 +++++++++++++++++++++++--------------------------- details.js | 11 +- manifest.json | 3 +- 3 files changed, 144 insertions(+), 153 deletions(-) diff --git a/background.js b/background.js index d58adf7..59bac43 100644 --- a/background.js +++ b/background.js @@ -126,7 +126,7 @@ function buildEmailText(full) { const attachments = []; collectText(full, bodyParts, attachments); const headers = Object.entries(full.headers || {}) - .map(([k,v]) => `${k}: ${v.join(' ')}`) + .map(([k, v]) => `${k}: ${v.join(' ')}`) .join('\n'); const attachInfo = `Attachments: ${attachments.length}` + (attachments.length ? "\n" + attachments.map(a => ` - ${a}`).join('\n') : ""); const combined = `${headers}\n${attachInfo}\n\n${bodyParts.join('\n')}`.trim(); @@ -369,35 +369,14 @@ async function clearCacheForMessages(idsInput) { icons: { "16": "resources/img/brain.png" } }); - //for the love of god work please - browser.messageDisplayAction.onClicked.addListener(async (tab, info) => { - try { - let header = await browser.messageDisplay.getDisplayedMessages(tab.id); - if (!header) { - logger.aiLog("No header, no message loaded?", { debug: true }); - return; - } - - const url = browser.runtime.getURL(`details.html?mid=${header.id}`); - await browser.messageDisplayAction.setPopup({ tabId: tab.id, popup: url }); - await browser.messageDisplayAction.openPopup({ tabId: tab.id }); - } catch (err) { - logger.aiLog("Failed to open details popup", { debug: true }); - } - }); - - browser.messageDisplay.onMessagesDisplayed.addListener(async (tab, displayedMessages) => { - logger.aiLog("Messages displayed!", { debug: true }, displayedMessages); - }); - browser.menus.onClicked.addListener(async (info, tab) => { if (info.menuItemId === "apply-ai-rules-list" || info.menuItemId === "apply-ai-rules-display") { const ids = info.selectedMessages?.messages?.map(m => m.id) || - (info.messageId ? [info.messageId] : []); + (info.messageId ? [info.messageId] : []); await applyAiRules(ids); } else if (info.menuItemId === "clear-ai-cache-list" || info.menuItemId === "clear-ai-cache-display") { const ids = info.selectedMessages?.messages?.map(m => m.id) || - (info.messageId ? [info.messageId] : []); + (info.messageId ? [info.messageId] : []); await clearCacheForMessages(ids); } else if (info.menuItemId === "view-ai-reason-list" || info.menuItemId === "view-ai-reason-display") { const [header] = await browser.messageDisplay.getDisplayedMessages(tab.id); @@ -417,141 +396,143 @@ async function clearCacheForMessages(idsInput) { } if (msg?.type === "sortana:test") { - const { text = "", criterion = "" } = msg; - logger.aiLog("sortana:test – text", {debug: true}, text); - logger.aiLog("sortana:test – criterion", {debug: true}, criterion); + const { text = "", criterion = "" } = msg; + logger.aiLog("sortana:test – text", { debug: true }, text); + logger.aiLog("sortana:test – criterion", { debug: true }, criterion); - try { - logger.aiLog("Calling AiClassifier.classifyText()", {debug: true}); - const result = await AiClassifier.classifyText(text, criterion); - logger.aiLog("classify() returned", {debug: true}, result); - return { match: result }; - } - catch (err) { - logger.aiLog("Error in classify()", {level: 'error'}, err); - // rethrow so the caller sees the failure - throw err; - } - } else if (msg?.type === "sortana:clearCacheForDisplayed") { - try { - const msgs = await browser.messageDisplay.getDisplayedMessages(); - const ids = msgs.map(m => m.id); - await clearCacheForMessages(ids); - } catch (e) { - logger.aiLog("failed to clear cache from message script", { level: 'error' }, e); - } - } else if (msg?.type === "sortana:getReasons") { - try { - const id = msg.id; - const hdr = await messenger.messages.get(id); - const subject = hdr?.subject || ""; - if (!aiRules.length) { - const { aiRules: stored } = await storage.local.get("aiRules"); - aiRules = Array.isArray(stored) ? stored.map(r => { - if (r.actions) return r; - const actions = []; - if (r.tag) actions.push({ type: 'tag', tagKey: r.tag }); - if (r.moveTo) actions.push({ type: 'move', folder: r.moveTo }); - const rule = { criterion: r.criterion, actions }; - if (r.stopProcessing) rule.stopProcessing = true; - return rule; - }) : []; + try { + logger.aiLog("Calling AiClassifier.classifyText()", { debug: true }); + const result = await AiClassifier.classifyText(text, criterion); + logger.aiLog("classify() returned", { debug: true }, result); + return { match: result }; } - const reasons = []; - for (const rule of aiRules) { - const key = await AiClassifier.buildCacheKey(id, rule.criterion); - const reason = AiClassifier.getReason(key); - if (reason) { - reasons.push({ criterion: rule.criterion, reason }); + catch (err) { + logger.aiLog("Error in classify()", { level: 'error' }, err); + // rethrow so the caller sees the failure + throw err; + } + } else if (msg?.type === "sortana:clearCacheForDisplayed") { + try { + const msgs = await browser.messageDisplay.getDisplayedMessages(); + const ids = msgs.map(m => m.id); + await clearCacheForMessages(ids); + } catch (e) { + logger.aiLog("failed to clear cache from message script", { level: 'error' }, e); + } + } else if (msg?.type === "sortana:getReasons") { + try { + const id = msg.id; + const hdr = await messenger.messages.get(id); + const subject = hdr?.subject || ""; + if (!aiRules.length) { + const { aiRules: stored } = await storage.local.get("aiRules"); + aiRules = Array.isArray(stored) ? stored.map(r => { + if (r.actions) return r; + const actions = []; + if (r.tag) actions.push({ type: 'tag', tagKey: r.tag }); + if (r.moveTo) actions.push({ type: 'move', folder: r.moveTo }); + const rule = { criterion: r.criterion, actions }; + if (r.stopProcessing) rule.stopProcessing = true; + return rule; + }) : []; } - } - return { subject, reasons }; - } catch (e) { - logger.aiLog("failed to collect reasons", { level: 'error' }, e); - return { subject: '', reasons: [] }; - } - } else if (msg?.type === "sortana:getDetails") { - try { - const id = msg.id; - const hdr = await messenger.messages.get(id); - const subject = hdr?.subject || ""; - if (!aiRules.length) { - const { aiRules: stored } = await storage.local.get("aiRules"); - aiRules = Array.isArray(stored) ? stored.map(r => { - if (r.actions) return r; - const actions = []; - if (r.tag) actions.push({ type: 'tag', tagKey: r.tag }); - if (r.moveTo) actions.push({ type: 'move', folder: r.moveTo }); - const rule = { criterion: r.criterion, actions }; - if (r.stopProcessing) rule.stopProcessing = true; - return rule; - }) : []; - } - const results = []; - for (const rule of aiRules) { - const key = await AiClassifier.buildCacheKey(id, rule.criterion); - const matched = AiClassifier.getCachedResult(key); - const reason = AiClassifier.getReason(key); - if (matched !== null || reason) { - results.push({ criterion: rule.criterion, matched, reason }); + const reasons = []; + for (const rule of aiRules) { + const key = await AiClassifier.buildCacheKey(id, rule.criterion); + const reason = AiClassifier.getReason(key); + if (reason) { + reasons.push({ criterion: rule.criterion, reason }); + } } + return { subject, reasons }; + } catch (e) { + logger.aiLog("failed to collect reasons", { level: 'error' }, e); + return { subject: '', reasons: [] }; } - return { subject, results }; - } catch (e) { - logger.aiLog("failed to collect details", { level: 'error' }, e); - return { subject: '', results: [] }; - } - } else if (msg?.type === "sortana:getDisplayedMessages") { - try { - const [tab] = await browser.tabs.query({ active: true, currentWindow: true }); - const messages = await browser.messageDisplay.getDisplayedMessages(tab?.id); - return { messages }; - } catch (e) { - logger.aiLog("failed to get displayed messages", { level: 'error' }, e); - return { messages: [] }; - } - } else if (msg?.type === "sortana:clearCacheForMessage") { - try { - await clearCacheForMessages([msg.id]); - return { ok: true }; - } catch (e) { - logger.aiLog("failed to clear cache for message", { level: 'error' }, e); - return { ok: false }; - } - } else if (msg?.type === "sortana:getQueueCount") { - return { count: queuedCount + (processing ? 1 : 0) }; - } else if (msg?.type === "sortana:getTiming") { - const t = timingStats; - const std = t.count > 1 ? Math.sqrt(t.m2 / (t.count - 1)) : 0; - return { - count: queuedCount + (processing ? 1 : 0), - current: currentStart ? Date.now() - currentStart : -1, - last: t.last, - runs: t.count, - average: t.mean, - total: t.total, - stddev: std - }; - } else { - logger.aiLog("Unknown message type, ignoring", {level: 'warn'}, msg?.type); - } -}); + } else if (msg?.type === "sortana:getDetails") { + try { + const id = msg.id; + const hdr = await messenger.messages.get(id); + const subject = hdr?.subject || ""; + if (!aiRules.length) { + const { aiRules: stored } = await storage.local.get("aiRules"); + aiRules = Array.isArray(stored) ? stored.map(r => { + if (r.actions) return r; + const actions = []; + if (r.tag) actions.push({ type: 'tag', tagKey: r.tag }); + if (r.moveTo) actions.push({ type: 'move', folder: r.moveTo }); + const rule = { criterion: r.criterion, actions }; + if (r.stopProcessing) rule.stopProcessing = true; + return rule; + }) : []; + } + const results = []; + for (const rule of aiRules) { + const key = await AiClassifier.buildCacheKey(id, rule.criterion); + const matched = AiClassifier.getCachedResult(key); + const reason = AiClassifier.getReason(key); + if (matched !== null || reason) { + results.push({ criterion: rule.criterion, matched, reason }); + } + } + return { subject, results }; + } catch (e) { + logger.aiLog("failed to collect details", { level: 'error' }, e); + return { subject: '', results: [] }; + } + } else if (msg?.type === "sortana:getDisplayedMessages") { + try { + const [tab] = await browser.tabs.query({ active: true, currentWindow: true }); + const messages = await browser.messageDisplay.getDisplayedMessages(tab?.id); + const ids = messages.map(hdr => hdr.id); -// Automatically classify new messages -if (typeof messenger !== "undefined" && messenger.messages?.onNewMailReceived) { - messenger.messages.onNewMailReceived.addListener(async (folder, messages) => { - logger.aiLog("onNewMailReceived", {debug: true}, messages); - const ids = (messages?.messages || messages || []).map(m => m.id ?? m); - await applyAiRules(ids); + return { ids }; + } catch (e) { + logger.aiLog("failed to get displayed messages", { level: 'error' }, e); + return { messages: [] }; + } + } else if (msg?.type === "sortana:clearCacheForMessage") { + try { + await clearCacheForMessages([msg.id]); + return { ok: true }; + } catch (e) { + logger.aiLog("failed to clear cache for message", { level: 'error' }, e); + return { ok: false }; + } + } else if (msg?.type === "sortana:getQueueCount") { + return { count: queuedCount + (processing ? 1 : 0) }; + } else if (msg?.type === "sortana:getTiming") { + const t = timingStats; + const std = t.count > 1 ? Math.sqrt(t.m2 / (t.count - 1)) : 0; + return { + count: queuedCount + (processing ? 1 : 0), + current: currentStart ? Date.now() - currentStart : -1, + last: t.last, + runs: t.count, + average: t.mean, + total: t.total, + stddev: std + }; + } else { + logger.aiLog("Unknown message type, ignoring", { level: 'warn' }, msg?.type); + } }); -} else { - logger.aiLog("messenger.messages API unavailable, skipping new mail listener", { level: 'warn' }); -} -// Catch any unhandled rejections -window.addEventListener("unhandledrejection", ev => { - logger.aiLog("Unhandled promise rejection", {level: 'error'}, ev.reason); -}); + // Automatically classify new messages + if (typeof messenger !== "undefined" && messenger.messages?.onNewMailReceived) { + messenger.messages.onNewMailReceived.addListener(async (folder, messages) => { + logger.aiLog("onNewMailReceived", { debug: true }, messages); + const ids = (messages?.messages || messages || []).map(m => m.id ?? m); + await applyAiRules(ids); + }); + } else { + logger.aiLog("messenger.messages API unavailable, skipping new mail listener", { level: 'warn' }); + } + + // Catch any unhandled rejections + window.addEventListener("unhandledrejection", ev => { + logger.aiLog("Unhandled promise rejection", { level: 'error' }, ev.reason); + }); browser.runtime.onInstalled.addListener(async ({ reason }) => { if (reason === "install") { diff --git a/details.js b/details.js index 1e9023f..f84ebf4 100644 --- a/details.js +++ b/details.js @@ -10,7 +10,16 @@ if (!isNaN(qMid)) { if (messages && messages[0]) { loadMessage(messages[0].id); } else { - aiLog("Details popup: no displayed message found"); + const tabs = await browser.tabs.query({ active: true, currentWindow: true }); + const tabId = tabs[0]?.id; + const msgs = tabId ? await browser.messageDisplay.getDisplayedMessages(tabId) : []; + let id = msgs[0]?.id; + if (id) { + loadMessage(id); + } + else { + aiLog("Details popup: no displayed message found"); + } } } diff --git a/manifest.json b/manifest.json index fbd37ae..36b94b0 100644 --- a/manifest.json +++ b/manifest.json @@ -24,7 +24,8 @@ "message_display_action": { "default_icon": "resources/img/brain.png", "default_title": "Details", - "default_label": "Details" + "default_label": "Details", + "default_popup": "details.html" }, "background": { "scripts": [ "background.js" ] }, "options_ui": { From c7333482ce0fe646e7eb601fa0f9700f648ac8ee Mon Sep 17 00:00:00 2001 From: Jordan Wages Date: Mon, 7 Jul 2025 22:07:00 -0500 Subject: [PATCH 05/57] Allow selective data export and import --- options/dataTransfer.js | 45 +++++++++++++++++++++++++++++++++++++++++ options/options.html | 17 ++++++++++++++++ options/options.js | 19 +++++++++++++++++ 3 files changed, 81 insertions(+) create mode 100644 options/dataTransfer.js diff --git a/options/dataTransfer.js b/options/dataTransfer.js new file mode 100644 index 0000000..b289c02 --- /dev/null +++ b/options/dataTransfer.js @@ -0,0 +1,45 @@ +"use strict"; +const storage = (globalThis.messenger ?? browser).storage; +const KEY_GROUPS = { + settings: [ + 'endpoint', + 'templateName', + 'customTemplate', + 'customSystemPrompt', + 'aiParams', + 'debugLogging', + 'htmlToMarkdown', + 'stripUrlParams', + 'altTextImages', + 'collapseWhitespace' + ], + rules: ['aiRules'], + cache: ['aiCache'] +}; + +function collectKeys(categories = Object.keys(KEY_GROUPS)) { + return categories.flatMap(cat => KEY_GROUPS[cat] || []); +} + +export async function exportData(categories) { + const data = await storage.local.get(collectKeys(categories)); + const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = 'sortana-export.json'; + document.body.appendChild(a); + a.click(); + a.remove(); + URL.revokeObjectURL(url); +} + +export async function importData(file, categories) { + const text = await file.text(); + const parsed = JSON.parse(text); + const data = {}; + for (const key of collectKeys(categories)) { + if (key in parsed) data[key] = parsed[key]; + } + await storage.local.set(data); +} diff --git a/options/options.html b/options/options.html index f40cda2..186e2b6 100644 --- a/options/options.html +++ b/options/options.html @@ -207,6 +207,23 @@ +
+ +
+ + + +
+
+
+

+ +

+

+ + +

+
diff --git a/options/options.js b/options/options.js index 2350efb..ca25eb4 100644 --- a/options/options.js +++ b/options/options.js @@ -2,6 +2,7 @@ document.addEventListener('DOMContentLoaded', async () => { const storage = (globalThis.messenger ?? browser).storage; const logger = await import(browser.runtime.getURL('logger.js')); const AiClassifier = await import(browser.runtime.getURL('modules/AiClassifier.js')); + const dataTransfer = await import(browser.runtime.getURL('options/dataTransfer.js')); const defaults = await storage.local.get([ 'endpoint', 'templateName', @@ -395,6 +396,24 @@ document.addEventListener('DOMContentLoaded', async () => { await AiClassifier.clearCache(); cacheCountEl.textContent = '0'; }); + + function selectedCategories() { + return [...document.querySelectorAll('.transfer-category:checked')].map(el => el.value); + } + + document.getElementById('export-data').addEventListener('click', () => { + dataTransfer.exportData(selectedCategories()); + }); + + const importInput = document.getElementById('import-file'); + document.getElementById('import-data').addEventListener('click', () => importInput.click()); + importInput.addEventListener('change', async () => { + if (importInput.files.length) { + await dataTransfer.importData(importInput.files[0], selectedCategories()); + location.reload(); + } + }); + initialized = true; document.getElementById('save').addEventListener('click', async () => { From 97628c693ba80c96d855cf2d79ce222b7e8d5448 Mon Sep 17 00:00:00 2001 From: Jordan Wages Date: Mon, 7 Jul 2025 23:54:52 -0500 Subject: [PATCH 06/57] Changing up imagery --- ai-filter.sln | 7 +++---- resources/img/average-16.png | Bin 0 -> 416 bytes resources/img/average-32.png | Bin 0 -> 794 bytes resources/img/average-64.png | Bin 0 -> 1506 bytes resources/img/brain.png | Bin 13993 -> 0 bytes resources/img/busy.png | Bin 3603 -> 0 bytes resources/img/circle-16.png | Bin 0 -> 389 bytes resources/img/circle-32.png | Bin 0 -> 724 bytes resources/img/circle-64.png | Bin 0 -> 1566 bytes resources/img/clipboarddata-16.png | Bin 0 -> 314 bytes resources/img/clipboarddata-32.png | Bin 0 -> 543 bytes resources/img/clipboarddata-64.png | Bin 0 -> 989 bytes resources/img/done.png | Bin 3543 -> 0 bytes resources/img/download-16.png | Bin 0 -> 345 bytes resources/img/download-32.png | Bin 0 -> 571 bytes resources/img/download-64.png | Bin 0 -> 1006 bytes resources/img/error.png | Bin 2921 -> 0 bytes resources/img/eye-16.png | Bin 0 -> 371 bytes resources/img/eye-32.png | Bin 0 -> 733 bytes resources/img/eye-64.png | Bin 0 -> 1394 bytes resources/img/flag-16.png | Bin 0 -> 300 bytes resources/img/flag-32.png | Bin 0 -> 475 bytes resources/img/flag-64.png | Bin 0 -> 800 bytes resources/img/gear-16.png | Bin 0 -> 462 bytes resources/img/gear-32.png | Bin 0 -> 993 bytes resources/img/gear-64.png | Bin 0 -> 2169 bytes resources/img/reply-16.png | Bin 0 -> 289 bytes resources/img/reply-32.png | Bin 0 -> 432 bytes resources/img/reply-64.png | Bin 0 -> 750 bytes resources/img/settings-16.png | Bin 0 -> 421 bytes resources/img/settings-32.png | Bin 0 -> 787 bytes resources/img/settings-64.png | Bin 0 -> 1489 bytes resources/img/trash-16.png | Bin 0 -> 390 bytes resources/img/trash-32.png | Bin 0 -> 631 bytes resources/img/trash-64.png | Bin 0 -> 1126 bytes resources/img/upload-16.png | Bin 0 -> 352 bytes resources/img/upload-32.png | Bin 0 -> 565 bytes resources/img/upload-64.png | Bin 0 -> 1006 bytes resources/svg/average.svg | 3 +++ resources/svg/circle.svg | 3 +++ resources/svg/clipboarddata.svg | 3 +++ resources/svg/download.svg | 4 ++++ resources/svg/eye.svg | 4 ++++ resources/svg/flag.svg | 3 +++ resources/svg/gear.svg | 11 +++++++++++ resources/svg/reply.svg | 4 ++++ resources/svg/settings.svg | 3 +++ resources/svg/trash.svg | 3 +++ resources/svg/upload.svg | 4 ++++ resources/svg2img.ps1 | 28 ++++++++++++++++++++++++++++ 50 files changed, 76 insertions(+), 4 deletions(-) create mode 100644 resources/img/average-16.png create mode 100644 resources/img/average-32.png create mode 100644 resources/img/average-64.png delete mode 100644 resources/img/brain.png delete mode 100644 resources/img/busy.png create mode 100644 resources/img/circle-16.png create mode 100644 resources/img/circle-32.png create mode 100644 resources/img/circle-64.png create mode 100644 resources/img/clipboarddata-16.png create mode 100644 resources/img/clipboarddata-32.png create mode 100644 resources/img/clipboarddata-64.png delete mode 100644 resources/img/done.png create mode 100644 resources/img/download-16.png create mode 100644 resources/img/download-32.png create mode 100644 resources/img/download-64.png delete mode 100644 resources/img/error.png create mode 100644 resources/img/eye-16.png create mode 100644 resources/img/eye-32.png create mode 100644 resources/img/eye-64.png create mode 100644 resources/img/flag-16.png create mode 100644 resources/img/flag-32.png create mode 100644 resources/img/flag-64.png create mode 100644 resources/img/gear-16.png create mode 100644 resources/img/gear-32.png create mode 100644 resources/img/gear-64.png create mode 100644 resources/img/reply-16.png create mode 100644 resources/img/reply-32.png create mode 100644 resources/img/reply-64.png create mode 100644 resources/img/settings-16.png create mode 100644 resources/img/settings-32.png create mode 100644 resources/img/settings-64.png create mode 100644 resources/img/trash-16.png create mode 100644 resources/img/trash-32.png create mode 100644 resources/img/trash-64.png create mode 100644 resources/img/upload-16.png create mode 100644 resources/img/upload-32.png create mode 100644 resources/img/upload-64.png create mode 100644 resources/svg/average.svg create mode 100644 resources/svg/circle.svg create mode 100644 resources/svg/clipboarddata.svg create mode 100644 resources/svg/download.svg create mode 100644 resources/svg/eye.svg create mode 100644 resources/svg/flag.svg create mode 100644 resources/svg/gear.svg create mode 100644 resources/svg/reply.svg create mode 100644 resources/svg/settings.svg create mode 100644 resources/svg/trash.svg create mode 100644 resources/svg/upload.svg create mode 100644 resources/svg2img.ps1 diff --git a/ai-filter.sln b/ai-filter.sln index 7922392..7818aed 100644 --- a/ai-filter.sln +++ b/ai-filter.sln @@ -47,13 +47,12 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "prompt_templates", "prompt_ EndProjectSection EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "resources", "resources", "{68A87938-5C2B-49F5-8AAA-8A34FBBFD854}" + ProjectSection(SolutionItems) = preProject + resources\svg2img.ps1 = resources\svg2img.ps1 + EndProjectSection EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "img", "img", "{F266602F-1755-4A95-A11B-6C90C701C5BF}" ProjectSection(SolutionItems) = preProject - resources\img\brain.png = resources\img\brain.png - resources\img\busy.png = resources\img\busy.png - resources\img\done.png = resources\img\done.png - resources\img\error.png = resources\img\error.png resources\img\full-logo.png = resources\img\full-logo.png resources\img\logo.png = resources\img\logo.png resources\img\logo128.png = resources\img\logo128.png diff --git a/resources/img/average-16.png b/resources/img/average-16.png new file mode 100644 index 0000000000000000000000000000000000000000..ff202ff88b987856d28c37c894f1a362af2b0b00 GIT binary patch literal 416 zcmV;R0bl-!P)H zK~y-6ozuH6MPU#H;BOjr z&X4U9JDKFqT3Pe2HM9RJBK+s8s%i>%1K*G>tY9*s8=OYOL-K7LU{*xbg+c7&1y?xA z>L#|2PM+Hlp%f3{9JiP+8z;YjdjvW?yo|7eQOrfeQ$wNjzLb+H){`qIb*E*iG6n$up7h@*nKOikV3^&9aaHX`D%VJ1<|V5^MK zWcyRBZZ`fh#DzbImvV?>pZ|kB?|c49Jd8W+)>uJyGS@v88^nt#KEvRL28HcTI(fl0 zj&YLbXY7{Zh)`iVSGd+&RX(O=?~kd<2B#76*su-#m~P7amwE%VNkkY)Rgm5Q0000< KMNUMnLSTY>O0#DG literal 0 HcmV?d00001 diff --git a/resources/img/average-32.png b/resources/img/average-32.png new file mode 100644 index 0000000000000000000000000000000000000000..e0fe4b46d0d2d0de6ca4ff004a33dd5acb80462d GIT binary patch literal 794 zcmV+#1LgdQP)7;9myn zimd}B5g`w87jOYs4v=Pz??46EqcKK-pZe?%@EBMEsyf#X z><3N(2MqQ-a2vR*+ui_~r4E!g;4^{@81g^BrM$Uf zVoZAi#)7zjW4CTyN`9;ja+`4JSOB{62)JgP+F7}u?RQMP%TWO&rS?_9FFQb9*7;XD z_XV#+{Qy%&?WPHrhg_eHn79)$0S^`QVTp_4!Opz?yy|4o8H`#TV+>hDh zvI6X?`^d01$^Dp3uBHGHsgoSD0D~FiW#E~TH-Mu#$!*M4vAu(rD6=Hnv$&mv%Sgf{ z^}QD>!%141I=t0Vi;y2SamQlBA1St55Q9IoM^623fa_9!; zjnj6>Pmmnx0=y&7ajcGGo`C5#$gdjm8qgoa1sp@ogpeGmBRLc9jq$4ihqCbNno_TC zZvf|k>p+Q?eIjX8{ibvfI8D-=rZx|p5s@lcW|Rl(oK{<23+zhP_=aiAqdTS~B{s=+ zk`kQ&W`NZI=?ZWk7{@EMJOU~)@{D)g1MJh$v(+SBmjn8vcoBFmA`6-C>mOM3&uAy? YFQTTm<6Y?dp#T5?07*qoM6N<$f-$sWFaQ7m literal 0 HcmV?d00001 diff --git a/resources/img/average-64.png b/resources/img/average-64.png new file mode 100644 index 0000000000000000000000000000000000000000..bec89df178cb32f906ae102e43d4ff8284711374 GIT binary patch literal 1506 zcmV<81s(c{P)d2v}$TK3Ik$)eORZ2I4U|Y@fQOcb7!y=VZ7c=t_^^B1y?f8h?(ELpyD2A`hdcM|Ip_O5Gqdw+ zmDZXESsrS10F9Ohh5)5hn^H=}>(@O2N~sQ|)DOS{Fr$=uvFiWzJO(JGI)HItccgkw zYu&l3QzQXOsU~1M&%o@<9zX+|wAOb4n({hI2%-fz2aLgcN)3K~O369&j9Z zG==&sa0R#u+#(qK0NQ}3fStf|1cPLfEC2&q>#q~~{AJ)Zt@WRxl^j{$7y`FtY6Un4 z9058VYUI&BqBGn|gg?TR?r#CMF7F7E^c%g4-*_8nUCrBB2SjAtH`7v zh6RH|Wy*Z)xl@Av$xH+Qo2VW6SL&U8*USs8Aij2(2U@Za@U{@Zxw3+f*q9WaJ!I0i z?F#P`>P}nqb4jixZM8@-?l)>%n7m9alU-%ae8g6_VB?Zg7a-UyjgP1^Wd|Rzc_U_L z8Q9>`1-hx-z5T`5j53N+s-0l@Y-;ugQa1azT#5bONU+ILBPIg23w75s1Y(r1&rbj^ z2!B`l{2v+3Ulr;kOhy8v{k>ZmgQ<}^X1*d4apIn(iGWt2Zc(UnmJs|H=FA_+k@<@J zEz~_CDHeJ-S`c_AF%ckTyDg8Sg27AKUXT<`XM>XRlUbwjILS#XQmj(9Ft&hMp>9`} zND3J6BRSx8;J6`D0`Ss?$Zeq6nSjvXkN{-Z zpqptM%QFF|1m7pD&mG@0g8voZrQ8IB4!#l`EMY#SEfW0m$VX$RQ$3&3$rjRIc`Fip7x0@9!#uFr z8v&uS!zBcN-4Mq+sqHvAwOxvuF1XoCnb*KECtD6qlg{UtfTIO~|H#niHSitKS;BRI6U2K-L-RoSNrc5^D(n;C!Rn7ogy^Jjxn!W9O{$*gaj=)DrF}=-9KpyZh zJ9~hW1T(U{pl^q^tgAX-1emVDW<8~C2Hpfd0WMLy(JGZ?;3Duba0tt0hL5s32r8wT z3GV*IzpeAOho_XbMPR1HyD0i+N!noavEHAS=V2Vrjd33?QoL(53T<1VIFT)r~m)}07*qo IM6N<$g6_7zO#lD@ literal 0 HcmV?d00001 diff --git a/resources/img/brain.png b/resources/img/brain.png deleted file mode 100644 index 296bb646386cd6cbcbc028e261e84bc130a313a6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 13993 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 Z{{!~PeOa9a)Aj%W002ovPDHLkV1jZT)9nBN diff --git a/resources/img/circle-16.png b/resources/img/circle-16.png new file mode 100644 index 0000000000000000000000000000000000000000..9984541714ea2e78e5bcd3c480ee3aa3f5be7ca1 GIT binary patch literal 389 zcmV;00eb$4P) zK~y-6ozuZCLs1Y0;P2kBs0Yy4X(jbN+v_h|m;SmbKAM*N2FBYAlS1uY(M`xI*APoxmk_kWFDK;3b^l6$7kSay=Yi zj3HW61gY~mZqTWAs7=^SHN$d}GO?FB&ji;<-q5cSTw(AVxI%J>Q7OO*Ce=63LC{N> zc8*ni;J#6&=D16kPM#$CPh^?i?Su zrjT)~ZlqI%NoJBH@GXv2jg*qDL^4+Byiv(*;wW`KQMYn!nfe$hbS{@^f(vY`EmQWx jIxqWH<}M;$n(96QSQkD23raJV00000NkvXXu0mjf2D772 literal 0 HcmV?d00001 diff --git a/resources/img/circle-32.png b/resources/img/circle-32.png new file mode 100644 index 0000000000000000000000000000000000000000..6023c0e97862d2eacd2b0fa78dad9ad5653301eb GIT binary patch literal 724 zcmV;_0xSKAP)%_D0Ty}iH~|ZU5IZgcI|x=F zfhZ3G7k~{&F?mRV0}#u_VruM4XFQn+)1swTX}Y`W|E8xOHBLnMpJgDggL7^O7zNG% z=e4;7>;fAi@+((B5jnB~RbUSI0JH(=Q`^8IFbh;O#XdrK0JsbM>?63NHL0%!OabL% z0RX3g1uy6la38n~oG@c6+V((a$@;tnYPkS_OTaf1WEHrPWlGh*4y+mfufRwq0HEfH zzW_>E#Z4RsJO(-j;?PVaee-zQJ3u3^a01AbfoS?`!XJFky!n%V6*qC8nYfKa0NrZw zYF|0dw5I@+fOWGws!0HIhBEVl%K#o+MzD;1#|K_;>3`iB20p64T zYqk%(1E%d(ORG)=G8fIBMuD>&{5jnr`Z@ibD_&lJqRU@oF-b>-c!^&C00002ElnGxQj3v*5<-m8t+phMQIZ;6xOam<)VMGb zKuz2jU!*S3s)<3xM|2}dqCzxjt8PTz1}a6M+}?gJ=3F>;?wq-&_s%(|H~3E`xidNU zoB#Kp$2s%Oj1m#9Wz5Q&K-S1q0HsuuQtF0GIV3#SHKMgyt z0h>kSTwKm2sG`+Z0)J4xwT=UC1CIgAf!Pk5vw?Qtao`>Fq*%`W@4#KD4gugPV8THv z59|RR1nQ~~xjwM5C$Je+-zDJ5#03Bp@PdQLRM6VXe2h<{?pEO4ppK~ew#}k_#BV+Nb!uyzuBrq8=3;-jsiD_iu1rtA~IGeFnhp#ma;Eu&3OP3$)Q7MVblR@!v&_Z zJr-Nvc;uBfzP6OD3TI3KS}lM0!5f|QallemUctq2CwKtEe!{AyJE%Yvl7Ha-op9B7o>mLbJ=yPmowzFt_2y!x?i395eE_Dy3$7 zr<4+uQjNe;qxfhzV+t^A(-6Y`cWGr-PY$FhW1&E90D4%cGAK5 zI^Y)z@4b=#aTx0aE?bQ4$l6f{XWJ2;9F8-Eu2p9vxa&Y!pNyNKglKx_emN(Lgj*95RJ zL+AS=T3&!nv20AEvTYF+3Hn&;^(i{ui2e9ZAw3t{)-)^L<*4*9usQ|j*F?PP3TfxG z>{UQutE2KM;60!%0ms|X7bjdtWn;H|#R3Fm1|lZLLdpXl01pB6RXE)MJPdq@`>^!I zxMSXn00Fr!^f|mJQiXIX=oOoQ4jf;S!PxEyp4}b%x19YEV$AkYsoDz@&A=Am8RB1b z$fE-lr@0!1ZbGl!7Zd;DMGn{rY!Q*lH*h?S&raYo%D0xUv=`{`!rh0FkPzMI@oGC@ zS#8Rp=arQ{IVW{(t_6J{sTX~@_Gt4QPGe3Bdr2sPl*ZlVQe^mr{58M2! Qvj6}907*qoM6N<$f_{k1&;S4c literal 0 HcmV?d00001 diff --git a/resources/img/clipboarddata-16.png b/resources/img/clipboarddata-16.png new file mode 100644 index 0000000000000000000000000000000000000000..6d615ef77b2fba225855802cb3e4f5d4e1b1ab23 GIT binary patch literal 314 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`oCO|{#X#yh2s3WX6WN&PEET=(wkgV~9oX+sTGpOojrk_gnTh zh;3?(a}?0k&3&`Cze3(SwO_G(>{F*T`lWsWGT58Z!e{`8q-zi8DBfr)$< z%)f+%e(y~Rtz{@UAo1bA!2|7cs$UwO&3?c$r=jzp=993M9$E%WSqr#BW^!sR5YKAc zxZWf0GSi+_?;0{6uRH(!ql5LlySoqhKhADEB=nH`x#D)7FFZRKy65|U5LbA8#OUj; zn^T1zAGxD<#UxiIFuBa}QmL3AJI{{WR^sjni&tIuxWgbjr+2U2RXanVml!-<{an^L HB{Ts5PGNe5 literal 0 HcmV?d00001 diff --git a/resources/img/clipboarddata-32.png b/resources/img/clipboarddata-32.png new file mode 100644 index 0000000000000000000000000000000000000000..a4268788d8922fd9d58f18a10f42b526d3681169 GIT binary patch literal 543 zcmV+)0^t3LP)h)4k#HJ?|ZI*!-!DiJG)!Jswy9p1J4Z-bJuF==JokhU=iFtG|O@{>zfH1KFY h4Q)vLIXc~*e*t55xZec^9X$X5002ovPDHLkV1mug>2&}A literal 0 HcmV?d00001 diff --git a/resources/img/clipboarddata-64.png b/resources/img/clipboarddata-64.png new file mode 100644 index 0000000000000000000000000000000000000000..9d945d01d1ba90f1d95bdec111e91d4d91aec7e8 GIT binary patch literal 989 zcmV<310wv1P)$Z zybZ`3?O6&JkqXe2Qp~)n{`O%{EL7Ed0S1AWz#^b2%Odas=+DH~44l8^!Rbcj=DH@p zJo&7g|5pZl+U#u-6cGU~`jXcK7mO}1kTlDnh*W{|z-eFy^^@vP;B{gJ0eY#QNLPSQ zz!Tt(s(wr?Jz=OJ~X?pgsbF-LhxFluxi1HMMuItXmEhhqeMpq}-jje-SWXGq`im_AcZJ~=qw zMt%5C#`HT$5EZ_ip8-z9=$nf4s{w6UtO8xshyP5pld3)wk%Pcl;4rYwK}Q^Bfj7Wi zRs9ju{u$^A{q8Wf&e;feAs{aMip`=_^*eCeL3SF%*1J> z9QFgcX|d}|upejhZvP7C42|nFbv9#%EOw2%v)B-6N&rDLEm=CDgud_q0}kVsfOcO5 z6n+lyMu4iW0K=BuCs@6KI%eu@Fl=;rf!9e;Rfk37IgXQ{s{Rs@qvS9NdS3;MfzC!6 zLLU0Irxgl62b6h0nFo}4K(SHg0lox5nFj!69Vfzg<{`+1OHoWB>-^wdd?NhWNSR)KfaADD#T#O4)T_#aT0 z3lz2)Qz-MghJDty)@8)mp9BO{fXC~=*=Tu0f?Ffb1cXSqO6w~#Yf(~NnMp)Ih=?k1 zgWwqbT9UC0yf?b{r12rut+6o7Rkkf>f+;dNSM_0UH&Ce;xh{xftxFvSZp(00000 LNkvXXu0mjfL+`Wz literal 0 HcmV?d00001 diff --git a/resources/img/done.png b/resources/img/done.png deleted file mode 100644 index b70d72805599e17c091d6b170b3dce3c55885407..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3543 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 RKqUYG002ovPDHLkV1h&PnS1~M diff --git a/resources/img/download-16.png b/resources/img/download-16.png new file mode 100644 index 0000000000000000000000000000000000000000..620f8687f8c8303c97a8af259cc528a254da2195 GIT binary patch literal 345 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`oCO|{#X#yh2s3WX6WN&PEET=$WUBV~9oX-OGl0M-l~CKiqdV zadDjDurxh@Ea`AHAO%wUI`zFWk8|S>2{d`uN zpB9%E6v}omV13G@w<^_pEtG4y&Ln<1FZ+k(f1tw2nn|}+xMTA-d#;Q&4MeC&2 zy@F=%Gs$e?I}Qg=+NyF{wl!^{*vB66H%Su5SE!vc=$hO1xohdFR~&C1?^(5Fv7qG< zPB;6feuV*{b6uqlCx6cgy?W@H`90f&KibP0l+XkKMly-e literal 0 HcmV?d00001 diff --git a/resources/img/download-32.png b/resources/img/download-32.png new file mode 100644 index 0000000000000000000000000000000000000000..840c82bc7d42bb4a1a21d5d9c971a6efebc4af80 GIT binary patch literal 571 zcmV-B0>u4^P)ruf3NrTnac=wmEFhr68~fmJlit{Lrra3?v;gaYh)!QXK)wq+&yri=gG5pe7zW+~ z5wK@wbCM$9J22p&i@=DBSAmZ%{z-g0{*p8X^Z@6;4`9X2E)%x^cm}L_D=GPFHy;Im zBJ^G0g~PT3*TMWI(3ZFbz`O@O2KIqvpxMJjz{cOm9WD?MV8qa&>;J<3FNHH@z14h$u z2|538J&aEHXapP*7YM+sf56r4#RsLJtP@JwChlCP#697pM&vEPC!iJBH?!LX21n%` za7J>ctJW`op|rc83z!0WiO+g1>>StzzL?ow8DA*ie_P#8!42$Q6^|nLNJIbt002ov JPDHLkV1j-8_PPK7 literal 0 HcmV?d00001 diff --git a/resources/img/download-64.png b/resources/img/download-64.png new file mode 100644 index 0000000000000000000000000000000000000000..67aa68df0ef30c4a0232edecfec0c03cb7d48807 GIT binary patch literal 1006 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 TIJ7S^00000NkvXXu0mjfV^C6t diff --git a/resources/img/eye-16.png b/resources/img/eye-16.png new file mode 100644 index 0000000000000000000000000000000000000000..392340e68ffc711ea339434e4587a3cd6441a659 GIT binary patch literal 371 zcmV-(0gV2MP)`@!!F*@$~oT;7+^aR$C!;+2OsF+GS;o29krXq@McgvEUmH_ z)R(Y;SFD!uM|eVWs*qwx(0;_`kZ-+! R5e@(V002ovPDHLkV1nL7os|Fp literal 0 HcmV?d00001 diff --git a/resources/img/eye-32.png b/resources/img/eye-32.png new file mode 100644 index 0000000000000000000000000000000000000000..5531c1a5252f0b50190e6493752d731548c55b43 GIT binary patch literal 733 zcmV<30wVp1P)tiUv|}w+qZfEDP9!ER*jKk=NFwNMI+7Dn8TB7x(ID&yF&OgGodQO% zF^9Y>B0Hp1McX3#<{aO37)~*F(G&~V5(U^@r7HR&+_cQvgVcVqivOO-e@h6VVkfND z9fzamNyUdGNs{Qj{HB*iQf7&c;Fi)6^yTbeo>CewV`@r#FxuJr9N+bk?;Y_Gq<{qq z7o>Ozx90>p8j<~uf(C^ikyP;6fuR)h3iI>Iut%X(iUBWTP6pG8V<~xDhF8#@!Oq2{ z2=gAxbNfKlL>@#=11Vu=g^JjrP-5MgwQd|&=#+7v=*aTrwc^Rxk6mbqmYx{hiU(4A zn^G%Yg(Z>CFiv1!2;plPSzgIRyF7z$nRM4Ozv48ORpY4jb37eu(%Fl}%Ks%s@J!)z zd=1w^2%pA`U5kJUf+k|7Lh6h6i$pI5YP}%cL9F@n@x_{OJ%mX P00000NkvXXu0mjfBB52F literal 0 HcmV?d00001 diff --git a/resources/img/eye-64.png b/resources/img/eye-64.png new file mode 100644 index 0000000000000000000000000000000000000000..7d7fbd2011d74575fe311d18ae099c025e684e35 GIT binary patch literal 1394 zcmV-&1&#WNP)W)xBMP=c00K@UYQl_02L(FLO- zQ_=@}iO7iPrbWGUqav_Oi=Z^kQbN--PlT7%!>og{U6(!kTvp)zV4n7T|L;HlHS^8P zH#;RkHS zw|GBI)6IRtv$q6Dl4Jm8<3?PE@x20k%(i14-p2YgO^@^nYMQ1!Y8ZhBuoY=PwcoH9 zr}dbB4+cIKFXEqm9C%~v!t$O75xoSGBpHbNvM(?)!gU9}#}@o0B)tRKZ^JUIkvI<* zVq66FUM#|jG))gj_{GftTq-1{sxNaC8}K-$V?+xxY$Rsl34GH;%oN|@l0I1in6G-e zZ^c76vkAPx!p<-iooyz3RYGG{Tw%AKm}GO?#9R2ENM(^D{+1W z#sJ)cFDf`K$DkewkPZHWk{~-UN4A|-U`@fsCZ_QrENMc_1at9w3D){55vsn{8ztb+ zVQ7=f=L#X|#bo%`t3=4Khv9`1oV8^*W$Lc(l{FR)M1MpMzPmjg5$AT{NS%lCGCE=e zSXuzI4U?NCm0_(dz}kaX@si^8d>B(AVoeoJjCoj#n-T!m;7AU@PFxT<_=yErt8r>W z-|5(u)AyCgm=g+cj$l@)0NHx_HOF}$ro|A+@E*>=dQU;p#AP{x?Z@EAxRbLP));%c zhP>72_)Lz|AzT?d_zdsO9K6dbeAeaoG`$Rt!A_xdI4TzbnUKVJm~rFh9w` zE8muQG{@(L7UM6@!RkV1cb{e|LOrP(<(kA?=SPqlh>uDJS=5eTc0`;xI9PzSM$y@$ z1aN?GAt@rb9K)L=HyWQt4E|r-8L`aDuIsOfy$l@(K1jGy7J=`?ydrgTupTSi z6^Eq^uLF0=T}S zCBO-Rk|e>|LhcwPl;4JDKkpX?y%)RFG~L!Or1YZz|5rQd|0C2Epl$+nTU&s-3Dj+E z0qQ1Dx3vYRn?T*x7NBkdbz572x(U>6C%pjw0TyUl8PSTrR{#J207*qoM6N<$f}z=i ALjV8( literal 0 HcmV?d00001 diff --git a/resources/img/flag-16.png b/resources/img/flag-16.png new file mode 100644 index 0000000000000000000000000000000000000000..3c2b58c46f67f386e6d384a0849d0e9ad531eed7 GIT binary patch literal 300 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`oCO|{#X#yh2s3WX6WN&PEETXs4%(V~9oX-HV2POo<{a58ks& zYrWagB*G@u=kSp6&6*w4F5Rx+)!!g0D#|KyTaep#9_yR5*odDSOD0`E^ThYwY)`?3 zPF^qNl=mk~3%c&TY;+^B;e6lIXc0lfl`}h5u&X}vUANHoA@gSWnm)#5Heev3N3eZzTh`PJ^aO*atDnm{r-UW|K{#>J literal 0 HcmV?d00001 diff --git a/resources/img/flag-32.png b/resources/img/flag-32.png new file mode 100644 index 0000000000000000000000000000000000000000..531dee24dbddcc7213738008845885a005265531 GIT binary patch literal 475 zcmV<10VMv3P) z5=>$cku?}}J%%@n<<0nZclL?!4h%Ez%{|OFcbE%flK4yY`6kS499RU(U9Qmt&Lq`S zXCu}NmA1n9~HpNhJbTm0C)!$Bt3^b(E!Y>1e^e)B&lDKbn}}pW(GEaSvTI5 zRLx?F2{7mS4`4s5DJH-=NzthQRWs|)6+qGzu;D(ZfWuq?oOXcL*)N;fTCM;}s?aKM z>wfK-*>uPgD#a}go7qI0EL$X3vI3l%*`%b$)W{5_I7Yk3trUCo>6^%M1V^-SuT#_n z?z#bZl2q#gAgH{Kpa3WU3V;IOe*!dI|JF+Y^SzGU<8%f%kkm*G>Loy4`UdU)3)QWZ Rx&;6L002ovPDHLkV1k1O$kYG; literal 0 HcmV?d00001 diff --git a/resources/img/flag-64.png b/resources/img/flag-64.png new file mode 100644 index 0000000000000000000000000000000000000000..64464154e79aa1bbc80c3cfde1b41a0b032b5c7c GIT binary patch literal 800 zcmV+*1K<3KP)Smrg}2EF`RgjcqzbD-l#8f>t6}q!QGq zD5#{fvR4p6C1_zIB>0G+kv$f>h)(XB%-q>MC!6_Uik-uG+>hOV_s(8XRhF|>SPx*W zRjG+0vIZE)I7=PPsp_|+OjV6qz!2~RXaeeFHQO@{MPE+>fH4Ob+-Z%)UQdb#>`ch% zmA1zwqyX@2oDX6Xl1(tsGhi-Lt%bB3SXZ%s7Z{JOmf!)C%>V4IkFArkfMMWQMgLd8 z+LW4}-_HL?M79C9fR#aL1~{myUsCEW2>|Wi91nqwL8u9gsOsC)+U@~}hyXW%9Tnj- zs`|K2efI!>)4-96{rkYBy2V`s5RqZvTt(|6O9B9N&lGc=3Lg`Z@0n`-PgB4&up!uQh{$tQy&hdL)2XnB^#j<|x2M93 z&>%1xo77JL03YA{TQ2=3&6Ol&Sa|9Q}xqFAV~#) z^#Il?0IUbFRsmo=fVBz$>jA7)09X%TtpdP$0BaQh)&p3p0I(jwS_Odh0M;r1tOu}G z0g#Z>T;$%8Pzr#T!RrsI`Z>oi7D@qd9(YRYAKCZ7(X12oSd#kmY5R%p*={_(+Djz^ e=oc;fZ-L*$`ho>)%P__O0000 zq2ZN#by_%Y9bPYXU|`P7oSEl5&p9GO+wd53)&2pNQEt?ZIs6VI46%B8(<#07+^AE9ZaNO@H-LRug)nr%I>FlK8}|x-m9*2&@@=a{9aEUY94tC*^adib2S(IR*SS6FBQ z2MPZId$-8=BGCliqtwaiB=bzZdDjoA@uzaYw)&s6CM;(6B|LTgjDIF4Yni+Ezt8mH zuf{yX%Ov>{Z`<_^ynh;%Qc{^gMEuCuL)?jouWfz*06^nayWlO6ZvX%Q07*qoM6N<$ Eg7A#aUjP6A literal 0 HcmV?d00001 diff --git a/resources/img/gear-32.png b/resources/img/gear-32.png new file mode 100644 index 0000000000000000000000000000000000000000..67300de32f7e208241ca8f76c9d526056736ad3f GIT binary patch literal 993 zcmV<710MW|P)I8XBZA{2u6)c+(2K;^h$ul!6pSM{#^~Tm7zPwv1mj8(@y~Io zh#K7aSonS*s6=Nm>S&w{W1{hqc+179ZRz-y;7?8et85QxWE@!OfR?%W z6~IN?g-^g$pxw#mI=gLUYov;Bz8hCv-P{E_L}V(^tg54d-4AX&t8|4()T@9_U=Q%v zqIhl#C2PjvJTcoxz!t+@GGPjUpMYD`=T?DVzh!a5+aAHG0vCYR8V7(W!2O890ErwX zoam}kM0w>|0|*z170N{shW@PYzwtot1AO&sfS)BVFcFx@IWCzSSx@hs>40I<%< z)&o!7PP9bqS5OexFfb=P1r-WPy);2V<&mzA$bWY7T@iUs>7&5rx?^Zc2TkccFg1`J zGudFt7UuyRjTGWrll6I-XDomg`;Gyh1KDIA4owtS|GQe$!plzE9tF-OQm{56U+3gI zBl1fWHBt$QXfDMT4FSs|04)?-`~=tli1m%YpC%gwW=8DW$qHP7XTUV?zyTAC0c{Bm z@W*hN+WWr>^px0pdhYQK9Jk3=4G>8W@X7(@{Uy+yuobt{y{g4^e8;}6B60#aLw3(U zRMoMdW=@I78B0OM!0rQARP{ZPCL;O!U_Tj+13b(DA*<4xk*HpzqDn;ki>nRa?_k%v zS8gS{DytEL3F$eJst`wkYrw)N5t-oyYc!D08*Y&iHy$J-up<-{K2GgV%K0RI0}d<#?gr8Rg`;b-0vic* zt^@N^_+kL1O+=c>PQ_9(9zU(B9|N5^z;3d8aQk%b~MIud7=i2PPW)`-Y^BC=gX9v6`%B63ufI-{zU6krap1<;)GCNL!jqaApl z0mgP9d>wj009iVkmIy|AYg8bAZ7DiYw3qoa7n*XhDn`FsH2k zQg#~@k=4LVrUtwN905!;{@(^nR@FU)6|Ymt-anme z&kO;J;DB>GfQOBCjmmmpYJ$-M%melq?JgU!DFFP~Q0Ui2yRpF21&rSebQ(C<8*T3{ zV?)_(@K~endA~wc2Z7nRkD4M zBzEf>1kM<)-cRW@8P^f9dc6vq9mER^v)hP|?d#>5;Wj>{^?IkcxEbt!{1Rnsd zJXFS~^jt)-Ge`}%B-a7Bc>9A$z&_wUjELWj!#h0{N>v+=09iu*PdV1WYd>d?Fwv zG%5cuL;mJG2i$Kcv4JL5`efMmZ)>T0VB+P#ZFs=bcMJjM2_Ja3EU`&Hvd&uA$ z045c1Kqv+GPu~WMxgB^h0B~)j0*eDZA2i-`!W#zvjghi14(Pre_+qAg0Hh5&O}#2o ziEe}U`~p3Afx*8blDEs??Ey|R+vfnalifpWKzoD|emwC3sW~6P-wN#TFt$aGo=-f! zkAagzJi(lxx?Fp=S1rJ#5Dy#(Z;$q>*Q@GK9-k+B2)FyB>6m)xoI}Tx%^NjciD+zzvbIuM6n@A}}s@7r-qbYXbm(Dlmjkz|Pw} zl;?!R_+#k3MCy3boJgmC{Sa0C=c|R2XUZCd{xTB!#skrdn z$Eg8d&vU>F2GcSkA~-{d3AiJzWsu@ddiBGQmFusUb-)o#$nV5HhW{qd0li-19YuTI z!<*K?xJG3MIZtXLfX04~Vc=QUp0tZ;8QzTJfJ>nM>6NXdW4!`&EiKr1e6 zOgDa?YEohofL{=Ip)r6<%oZN@hTIlo1a8Tj0?!%>MV$a66`BOx2&}-JGipiMihEaZ z0|kz~5%T66_WUi~#!rji4E&sfsRrE3f6oQu60g3Zsyiq_^>>4+y00XT0H@(X_gzW# zMYxgV2SAHe^_K^Jc^(^&yF7@Qqcz;6#8<28u_k)>RA4FbdB<(Qk-;gDfL=ned%;0q z(FhoSHF0V7beW&Q34qUQ0(`+CG~T-KXf#@XW@6+zx*E6}_?q|kXk6shB_hj`*J&16>A0v77oxHb*kaUWR%j9F1fBuD z9D#W$@JJv9{%xbKHv%(EB^RYRWEZ79xE-P;X>&UHx5T0HFwl`;v;aTBt@wVsQf~-K zeJ~*+?YNI3zF_?QQBqFESzv1@e~8N}or3)`eU+-tDy(>#D%hGXqxhS` zM@VI869fNU5MwVetF-N&PEETXq~5vV~9oX(aVN@PK*L97tEWc zUW=G@;b!*3M4qY3A9w^+@GffI%Bp6j64uhj79-^|q3yrDn%}0gd1YnklRQH$S<}^X z|64jv0z&mB73V#VHeThn7GD3cQGHJM)McVE4_Uc=KGr^YCUZXY>o;#F?mLOIQc5%u z+dPFiD-H;W|30f|XPD=3;V{?tzqZEdo_Vt~R;==!B&zqDy?MP@+?tJk*L75O9;{Ap elK)dbV;k?Mj-N4#f0qMY&fw|l=d#Wzp$Pygd2JQ| literal 0 HcmV?d00001 diff --git a/resources/img/reply-32.png b/resources/img/reply-32.png new file mode 100644 index 0000000000000000000000000000000000000000..193deacfb988ca7da2d88811ee8fd6616922c4d1 GIT binary patch literal 432 zcmV;h0Z;ykP)LBxownu3nX=b;}DH5un+Vi#OxKgxBZ?QLZFqp z@~6}|R=oiiz}ny)sHzXl15Mys(q8&R(IYqjo+Fqq09(Kzun2qtJGL)@KSQ`2E$z?% z9>U&TV0zfsFeXGZwfANwKv-BxBLZgWzZC|Kazkjl4V;GQ2CzC}4w8L8fX6Uh0hlRR z+?I6PANno^K+sn>st_PRoJTay)MWn;l%%A+zd))HRV7L5z&3DZyH`2Dp#EhBYJeKx ahrR(F>X`>a$?AFl0000^6 zZ_$_&m7#35GKa-mRA+IRSyJX(aSt64?%XK}!rEIme2VU9O~_m;@X_Ca^W~EsL%~H) z(hI8<-<-`W`*yDS`=-waM64%E*S+6ty?b)(WS7}9Kk+^A6?!0hV7AHu>jrbrhH{4d zNeu59?x`@|W8CA(w1>IEcnX8@Y{Shn16w~X*v4SIo4urQ-ORhPu}chM8mVgW&a#0q>b-8vD}JGm>nZsI{x z{RH71Pxkh;XRg(&i)Uot`y+@kS7Tp*Lv+{lmQ>dFNom~&<{tQ4BwfK&v!Z^EoW>!x^4e~;s+_wtY25X^pH~OVAPS#FVdQ&MBb@07w`^00000 literal 0 HcmV?d00001 diff --git a/resources/img/settings-16.png b/resources/img/settings-16.png new file mode 100644 index 0000000000000000000000000000000000000000..bb7581d506458a18599594975402ef774d06ca03 GIT binary patch literal 421 zcmV;W0b2fvP)~3* zK|~DW5I@ku9BP?))>i;CI4WIX48L)TA^druwuRS~%={J+bxdLxYq&ra?{VHopdmB= z5sTQz0hYURukf-nmYH-1dJ$X5!cU#KN<=)zI>s^5W7H~^Fk3unM8p-E_*g-^>~m4r zjfv9vya;45ehSt9UEvEZ3qRlU03UFI?|9ooAW$!i&Rn2|uXu;2*uWb%x6lul*o*RY zJ9t)%Y~mNL2MDw=AEl^)pBOF53z_-Q1>Qkqrihrv3a+p{=>G2sJO+OOP)yR=mcdHVa5gi?GU>cEq7RS+3w{In07yO-4;KlxN zR{`Kr%;aES;~DJ1r+Mujo^1j^iU=vCL41X66)Fux#I;gBOyXhO!0)S6{x;r@h;P~_ zex(D|^LStpJ6CF?S&4s8W+34J9?1wF$B8_CEh5fj2A>T=6EWs)Od{zp2N71_5^YJAmNGUyyhid8SY)|0BJRZd{B)p4}y#4|% z=ke``m|c(nuUAmF8lP6WC(w((E54h<{)kw(j{j1$*R@(p(Y)GrH~V7i#SZdy91&wF zrN{8#DgzJUtRfNL#Ob^~f-QM`0zc&OG(MMuH<5q|-};y0pYU^Oevu;Ix>*G~@SXqb zJ%R1ZDnq=K(N5zJ#r3;$1t5Actf+)h971pHp3>$cibPy|f*HKn6jPM|@TB5~3jBzp zIEg>=+I94G13>#x`Z6}<(>@ou*Qb;&;7e@Bdnu*K<&?xSYzm)5M61ofNBN|eD>SW= zr3oJ{GjJpS4y2U&BVsDgcPmN^ZuxJMS&RLqsE&mk7*uquP?XZEIIMVhMBcEuYe|?v zoa#VT3kSPe1tk!}iY^uk?I$$!{O;tHFQWYXo{xyRhBWVJwNJA9Ve9_y_#YNnS=U7p R4GjPQ002ovPDHLkV1lC5YoGuC literal 0 HcmV?d00001 diff --git a/resources/img/settings-64.png b/resources/img/settings-64.png new file mode 100644 index 0000000000000000000000000000000000000000..347d454ca9f7c20728838a6404877bf507b3d2e1 GIT binary patch literal 1489 zcmV;?1upuDP)`A~XgeL@}<6@gqi~)QwSeff_$H zMi%PE7=xlFd;~xE(kL5r!{=1>b=LzCX~yjR^JBwrEAaGs%FG&Vz;(qRwgbNM6jm=H z7gykE@syc06c`PfT?#*sNoosm=YVFQ$r^hAc*CVB$|)UbXPN;Dz>BK7uQ=fQ8C(Wz zw1$rX3srTO#XAMqgh{Az>;gJHCS6M$RMo!tfUg540zE*h^=Jrq4^syWV9p3DfXf|X zma6J!$>*FgQdEM=NuPmtg#PZ^~!mfB-L({7lddw1kef z)0V1UhFdU6t(4)1foFjF(1w;GuOwIsB60<=0Q1eCf+;z713Oi9xJudcfqOB|o*4}v z#GEtMscLsM@`Fd9%<5_ZKBe;Z+62rBvIR0Uz8&aGV0@7d0qw!IKrJC6t(b;tqvxxC z1J`S;%1eSgw#C;_+W5l}xiAfQ*IQfY5(lKcK2ZV@9k9yhZqiMVy9!rZOw0 z85s7E_ZQX*i3>X~QW)CkIPFfbZMr>W{y}sIR2_R37{+vT^Mn%#c#e6>oPi0pf#uBy zf&T~}`63MflgdPY5^&U0=EkyhfQZZlW_xll{)z@6iBJu*}P!`92Od0ON z9^g$?J-KQ^fBLM5K8iaLt^n#{e07+vk}Cl|?4vVRm7s`B!gQQ2b3E?FoIC|{Fmmzi z2WG44U}B}AqwZJ*9r%6PiZl!?^3|8SQdm{DVU~Bn>w<|@3L_eX&D zfvKlqMN9?eL{u|D(v}WyuEXY*|bpT(G{Cd2Iy}*sCI^g+UP>lHsh)4tQ zA#US`F?aoaj%Q%1N4qM^Kdhzy67GHeD$o$)n-6^F;2k8V{r^Nc0QkjW)Kew$L_O}K z=+mK%O636HQim};z_^kxo4bHop8m3=0ZePHv6KVa9WT98)_dmv1$k?tRHC(U8{$cD z0;gigF&4*2L}wDNj{k@|;E;p)h6=*w0TZj8(P_pv@gJjgK-3XFh-n$cu|!orx1~h< zTQIHcIC?Rc6nR4d*a56k)sw9@Yfrnn!bdUp`V!;22w3ajEdXtyGrJGjf0S$ie#Z3L zZ0y4vN+x3(5|bS~>s57m+7JbSBeQpsF1z+q#Vu{{I>2(mYER>j05^p;re+R^obVW? zc&KE27jRW*L()>&rl^QC0#9P@-{!_1odCYZ{IPn2mJ^b5B<>1F9otJV2cG^j+2}ZO r)fNwd+;xRdH_kaA^aMiVtarfwH_=Tv5PQ|!00000NkvXXu0mjfGS|6a literal 0 HcmV?d00001 diff --git a/resources/img/trash-16.png b/resources/img/trash-16.png new file mode 100644 index 0000000000000000000000000000000000000000..43e1f7fee82f83c47d98c64decbdd3c8063a9868 GIT binary patch literal 390 zcmV;10eSw3P)@wb`yx|FqUDvm1Ao99aG$MpCu86R7}a_J-1se)OVt^3<~sRKLgZfu z+WrcKnO(RR&a|Cb9J~V10|cD9T>^KmrTfmD={98m&^c0n+Dq&|2+#_fOR5eh=o6TQ zHB?R9P)%k%fFzmO(jg?>hTv)40Tv1$D3$>54O|^5q3_I2_6Fq{SW_R_sB7RUp0WM! kY-T;+vLS;4ManZK@rxo4-h1hn{Qx1a!yX-CF+9+K7e>he1lvf=3p*K z4qjCPLM}q02BJpX@-Q{hHr+eZGb0j6K~rqkzi+C(-fmGbPjnSKMG zRCPNkp{kZzKnvIav|z0RtCdWq85reXV8`<<6ve$XEoRBO(J;D_d;44gi2*FX^C&Oa_WgipZe1?Z%!r0swdi?1{*{Q+yuS zGg}NmQU_eLJ`bim9owq~pkD`4Vh&U=8>RY&v_JwzuZ_=CK!+GgvHkjUJO-qsuj};uXvBtIC@&A43xy z2+kkk8~`{mJz4|6i0RWR0KS=?6Opmh$|5ojytJQRqJH_SIFMkP>>bx4WAKke3{S~U z!4>daRd<431tfZ7#jC!4&wvTNyz+R7IS9qdHm}t}Oi0lI-=QdT<Dxr8kKnJ=7d*gG;ChYvHc@*9h{!bX#JIEs zJSIx(OclT^u+NbzFs-V8R(3Lh5PyMFhU^dEgsT2kM1hDL0PX;DNpY;ZRsur20!Iwl z#iGFjsOo#*qH*bHX|GZcf`HEr+3ON|UKz44O6bW%R|%$F5Rn<+3h>7_$(oM1B@ol7l31_3Yx)h^w$R;fXT%9s;YiW zYn)jL#tk`;z|&yY7bxgNQ)oN`J^&j9oJhib zA{y(lbh{Ikc~8@=12#xZd;7kHk^@fNDa1wY5{E~6|k3{2w-vGB%^+t{_m1c#Y#6n3CSp*IyL~m7f+}Cg& z5Rtd^uVGqL)k8iy%1mpyEN`X0fOiJeLBLk}3$#MOurN&PEET=&h%VV~9oX-OKB}S^`B_KHM+d zzgCR({{qM64uz(>54eA@ud&ii>+UflPDYerC>??JJC&ZvbPjQnAql| zrP(iN@kx~aeDqI~^_>ShTh54{{c<2Yejm%)M}ns<4r@;1+J5cI^=JtNWP@y|RWA%5)A_)!U!=6OWa3ba%Tg}*|9=twC22}CI&L4ryr#PcK~ zB7W4Okq|`Wy`$K3-=4j@>+Q;A6+6i!v**m6`R?2~b9N)8WRfy7(d<}VUxmaYNZDwq7@Ohv$jXb zRmSY*D!OzHe&}@R6*@BPc@`II zucI|m>=6M@m&uEZYZ)M~l@Mo|1endGJzu*ydjRtK+3Y*l7{CyyfdJ#`e>DlG08@Y| zz!YFySsY@IC4GO*#*i1dgP(Z)r$PFD5BzJHcn|yn?X6@UmO5cF00000NkvXXu0mjf Dma6|+ literal 0 HcmV?d00001 diff --git a/resources/img/upload-64.png b/resources/img/upload-64.png new file mode 100644 index 0000000000000000000000000000000000000000..65629dfb4ae1193db0e6150f47a5887cc9c27d32 GIT binary patch literal 1006 zcmVXdZ>uB0FMYdPJn|V(xIw-89ic&!{^E=v;w|3 ze7jTA$%7+#fMr0hh_q+%44}o~`|P|jq@kz*#wczm&H`0sG%CA*QAgYvn*jEg*$f>6 z{uIQW2I^hcJKJFZ5xE0&I);XTcg(iK!@xCFeeMi28?P03$p16%!UPbx97JRra1L0P z=iW@v5*MOhRok3_B96m;67SUI7qF#_nNL&+KrQe+W~?coIW$&8IUT0BoWksQNQ_q8 z%PGpnfjuEH4>na^+b# zQu1k}aKE#*$TnqVE)0!tIl{7sKOtN3QTTe5 z)^Wj~2zdd#0M-Po;RUcJU=1&TH34gQ0jvpF!wX + + diff --git a/resources/svg/circle.svg b/resources/svg/circle.svg new file mode 100644 index 0000000..84f078d --- /dev/null +++ b/resources/svg/circle.svg @@ -0,0 +1,3 @@ + + + diff --git a/resources/svg/clipboarddata.svg b/resources/svg/clipboarddata.svg new file mode 100644 index 0000000..aebd8f5 --- /dev/null +++ b/resources/svg/clipboarddata.svg @@ -0,0 +1,3 @@ + + + diff --git a/resources/svg/download.svg b/resources/svg/download.svg new file mode 100644 index 0000000..595f3a5 --- /dev/null +++ b/resources/svg/download.svg @@ -0,0 +1,4 @@ + + + + diff --git a/resources/svg/eye.svg b/resources/svg/eye.svg new file mode 100644 index 0000000..2f1f62f --- /dev/null +++ b/resources/svg/eye.svg @@ -0,0 +1,4 @@ + + + + diff --git a/resources/svg/flag.svg b/resources/svg/flag.svg new file mode 100644 index 0000000..ef18dc3 --- /dev/null +++ b/resources/svg/flag.svg @@ -0,0 +1,3 @@ + + + diff --git a/resources/svg/gear.svg b/resources/svg/gear.svg new file mode 100644 index 0000000..3827d6a --- /dev/null +++ b/resources/svg/gear.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/resources/svg/reply.svg b/resources/svg/reply.svg new file mode 100644 index 0000000..fdf9414 --- /dev/null +++ b/resources/svg/reply.svg @@ -0,0 +1,4 @@ + + + + diff --git a/resources/svg/settings.svg b/resources/svg/settings.svg new file mode 100644 index 0000000..ce5be79 --- /dev/null +++ b/resources/svg/settings.svg @@ -0,0 +1,3 @@ + + + diff --git a/resources/svg/trash.svg b/resources/svg/trash.svg new file mode 100644 index 0000000..c236be7 --- /dev/null +++ b/resources/svg/trash.svg @@ -0,0 +1,3 @@ + + + diff --git a/resources/svg/upload.svg b/resources/svg/upload.svg new file mode 100644 index 0000000..09e7ce4 --- /dev/null +++ b/resources/svg/upload.svg @@ -0,0 +1,4 @@ + + + + diff --git a/resources/svg2img.ps1 b/resources/svg2img.ps1 new file mode 100644 index 0000000..c71f0ff --- /dev/null +++ b/resources/svg2img.ps1 @@ -0,0 +1,28 @@ +$svgDir = "./svg" +$outDir = "./img" +$sizes = @(16, 32, 64) + +# Ensure output directory exists +if (!(Test-Path -Path $outDir)) { + New-Item -ItemType Directory -Path $outDir | Out-Null +} + +# Check for Inkscape +if (-not (Get-Command "inkscape" -ErrorAction SilentlyContinue)) { + Write-Error "Inkscape CLI is not installed or not in PATH. Please install it from https://inkscape.org/" + exit 1 +} + +# Process SVGs +Get-ChildItem -Path $svgDir -Filter *.svg | ForEach-Object { + $svgPath = $_.FullName + $baseName = $_.BaseName + + foreach ($size in $sizes) { + $outFile = Join-Path $outDir "$baseName-$size.png" + Write-Host "Converting $($_.Name) to $outFile ($size x $size)..." + & inkscape "$svgPath" --export-type=png --export-filename="$outFile" --export-width=$size --export-height=$size + } +} + +Write-Host "Conversion complete." From 58a2891847eaaf7991ba72ef8cdd3aa3e2895a30 Mon Sep 17 00:00:00 2001 From: Jordan Wages Date: Mon, 7 Jul 2025 23:55:38 -0500 Subject: [PATCH 07/57] Update ai-filter.sln --- ai-filter.sln | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/ai-filter.sln b/ai-filter.sln index 7818aed..f41f23f 100644 --- a/ai-filter.sln +++ b/ai-filter.sln @@ -53,7 +53,29 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "resources", "resources", "{ EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "img", "img", "{F266602F-1755-4A95-A11B-6C90C701C5BF}" ProjectSection(SolutionItems) = preProject + resources\img\average-16.png = resources\img\average-16.png + resources\img\average-32.png = resources\img\average-32.png + resources\img\average-64.png = resources\img\average-64.png + resources\img\circle-16.png = resources\img\circle-16.png + resources\img\circle-32.png = resources\img\circle-32.png + resources\img\circle-64.png = resources\img\circle-64.png + resources\img\clipboarddata-16.png = resources\img\clipboarddata-16.png + resources\img\clipboarddata-32.png = resources\img\clipboarddata-32.png + resources\img\clipboarddata-64.png = resources\img\clipboarddata-64.png + resources\img\download-16.png = resources\img\download-16.png + resources\img\download-32.png = resources\img\download-32.png + resources\img\download-64.png = resources\img\download-64.png + resources\img\eye-16.png = resources\img\eye-16.png + resources\img\eye-32.png = resources\img\eye-32.png + resources\img\eye-64.png = resources\img\eye-64.png + resources\img\flag-16.png = resources\img\flag-16.png + resources\img\flag-32.png = resources\img\flag-32.png + resources\img\flag-64.png = resources\img\flag-64.png + resources\img\full-logo-white.png = resources\img\full-logo-white.png resources\img\full-logo.png = resources\img\full-logo.png + resources\img\gear-16.png = resources\img\gear-16.png + resources\img\gear-32.png = resources\img\gear-32.png + resources\img\gear-64.png = resources\img\gear-64.png resources\img\logo.png = resources\img\logo.png resources\img\logo128.png = resources\img\logo128.png resources\img\logo16.png = resources\img\logo16.png @@ -61,6 +83,18 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "img", "img", "{F266602F-175 resources\img\logo48.png = resources\img\logo48.png resources\img\logo64.png = resources\img\logo64.png resources\img\logo96.png = resources\img\logo96.png + resources\img\reply-16.png = resources\img\reply-16.png + resources\img\reply-32.png = resources\img\reply-32.png + resources\img\reply-64.png = resources\img\reply-64.png + resources\img\settings-16.png = resources\img\settings-16.png + resources\img\settings-32.png = resources\img\settings-32.png + resources\img\settings-64.png = resources\img\settings-64.png + resources\img\trash-16.png = resources\img\trash-16.png + resources\img\trash-32.png = resources\img\trash-32.png + resources\img\trash-64.png = resources\img\trash-64.png + resources\img\upload-16.png = resources\img\upload-16.png + resources\img\upload-32.png = resources\img\upload-32.png + resources\img\upload-64.png = resources\img\upload-64.png EndProjectSection EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "js", "js", "{21D2A42C-3F85-465C-9141-C106AFD92B68}" From 6c352e904e146fcf3f619d093f6905ac041aaf0e Mon Sep 17 00:00:00 2001 From: Jordan Wages Date: Tue, 8 Jul 2025 00:24:43 -0500 Subject: [PATCH 08/57] more images --- resources/img/check-16.png | Bin 0 -> 300 bytes resources/img/check-32.png | Bin 0 -> 450 bytes resources/img/check-64.png | Bin 0 -> 927 bytes resources/img/circledots-16.png | Bin 0 -> 394 bytes resources/img/circledots-32.png | Bin 0 -> 773 bytes resources/img/circledots-64.png | Bin 0 -> 1656 bytes resources/img/x-16.png | Bin 0 -> 227 bytes resources/img/x-32.png | Bin 0 -> 331 bytes resources/img/x-64.png | Bin 0 -> 655 bytes resources/svg/check.svg | 3 +++ resources/svg/circledots.svg | 4 ++++ resources/svg/x.svg | 3 +++ 12 files changed, 10 insertions(+) create mode 100644 resources/img/check-16.png create mode 100644 resources/img/check-32.png create mode 100644 resources/img/check-64.png create mode 100644 resources/img/circledots-16.png create mode 100644 resources/img/circledots-32.png create mode 100644 resources/img/circledots-64.png create mode 100644 resources/img/x-16.png create mode 100644 resources/img/x-32.png create mode 100644 resources/img/x-64.png create mode 100644 resources/svg/check.svg create mode 100644 resources/svg/circledots.svg create mode 100644 resources/svg/x.svg diff --git a/resources/img/check-16.png b/resources/img/check-16.png new file mode 100644 index 0000000000000000000000000000000000000000..2a829805b3a1cd134853c1ad26ec809c1048a6c7 GIT binary patch literal 300 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`oCO|{#X#yh2s3WX6WN&PEETXs4%(V~9oX)yuY7PJsff5At0u zbxbhPpS!G3_}zh9u1mz;Y-+f2v#B*Zfa6BDZxB~xW2!=e;`L{m&t86J=l*y|(nd_4)D+W|Fx{` zSH?7sJ;{yN;_}loIxIIa{S>XwkQdRfka^7AZ+Y#Y(A}Ww*4)xF29DDEU&x+)-f&-M s`=&$3Pd0cvRUeerTF&z5UwaF1JXNji0(O88K!ICe<=0Gw@DA{7@F!q1b*>`#DsX4;cVIhnwgUJPaBc7} zV1Md-$?z6%VQ>WwbN8f#w1K3cz*zu4joOnEa&7?wC;T{okAPNDf$$afBjG(eZw0P^ z#c1)Auw`Hzd_)I1@Z7GuKSE$FsL=%2&6tbox?8})H-JYAfBo5IPuB|x7-S2037)WH z_g!5tEMTy1aK;b7y0_~y37Etl@M*v|Z`WrQFo}Kuc6GfJ0h1UQ{L<6)X_r`V^b(N- sxL4JQYhzZPSD@_mfBuRafj=wo4O$HW=K^IzoB#j-07*qoM6N<$g5e&#HUIzs literal 0 HcmV?d00001 diff --git a/resources/img/check-64.png b/resources/img/check-64.png new file mode 100644 index 0000000000000000000000000000000000000000..175c4a276691fe7854413dbf6b0ec13e843c6a3d GIT binary patch literal 927 zcmV;Q17Q4#P)7@5gh`@7;4nRoTpH zvNm9?9Dua}Yvll}4OlA&U~Rx!IRI+|*2)1`8?aUmz}i645|M(442Vd3RUK5dVJ_f0 zpzitsbXMxqFe^YrhJdSU{01%Nr;8v`@Sw9dFkpl1%*jCc?3plK*)8+bS zdIg9`2Qbe1@Csb06dp1RKt$SrXTa`=<`GpLt7;(Yojc}kzXh0Nb;na~?3W&%cFdES`3v{dMmzu}S3IGx50UiM@B~1$y%iT^n9G2c9}F9`4|OmzZF5vKkCCld@`02U+WSptp*TX9vI>ve%k%`F4R z6A0e{eDr6MngBSn!ycnw<2<9JRO=H1 z;7t2U)JI@X4dW#GUY{la0q%N?zXNvG2{P7ueQE%l`DWmuC%{yt0KlN9Z;)D_H~<%7 zEAYw_;;2<0XzIpz`A3WjjScX?k_*7U#x`7w0|5O#Fr@$#y zT}jM93?+Ibrm9oGS&DCT<@5$PV;vp>oC&JxW8fmiB_5?&pjTBFlb$<{f*gvf>OJ5} zMAHIrLSuZu2&SMrEL>yt4RQfEV!i1F-2|f~B8Pwjz;jid3p!a0W#-dZC~fEuBWWlH zU~Rx!IRI+|*2)1`8?aUmz}kSdasbu_thMO@`~w5OS?n{{`V0U7002ovPDHLkV1mW! Bk2wGU literal 0 HcmV?d00001 diff --git a/resources/img/circledots-16.png b/resources/img/circledots-16.png new file mode 100644 index 0000000000000000000000000000000000000000..1443d226c15d61a0f9fa7c9a5e055a1d468258b0 GIT binary patch literal 394 zcmV;50d@X~P);L)DsdN}nXE&|t}_FxN( zl3MDS>i?^{|F04e#ziS*0iFDOj)<4B2P5KVpnU=$~Snxa$TX-VJFXYn@*a9Bhonm!KEi-^~TanH~s zohhoX7|OWaHqw=$NhXpc_z@RsBV}?nNj?m9UaxW+IL*qJ+E#82(=P7vk(yx|;1)Yt o!_;X1D@rNz*)lvu#M`)Y-~5wsLN!KCO#lD@07*qoM6N<$f}a1N?*IS* literal 0 HcmV?d00001 diff --git a/resources/img/circledots-32.png b/resources/img/circledots-32.png new file mode 100644 index 0000000000000000000000000000000000000000..e8dfa44211444bbbf9660f7e39b74fc6ac5ce183 GIT binary patch literal 773 zcmV+g1N!`lP)@(^qejM*MXzJF15ELx*HNDZSw-Cq#^(u0lp|F?|@TDLMrw(;Jvc{3=AhC z08~8iM?fJdxbkCwyFgo!*b<4*nMYx71NF4P0V0!%M8nSsfAcl<<@f#$9oTw%NwjKxS-05Hbv15V;( zj={+jnQL;TI|9H7;&YPf@`1hYRPn8*uN!H#={7!;*4BXu|IAYnP(;eWH1L}Kzh-}d zSHP4mwSL10Kw^>ZX$&|>_s{7j?&tJRDt~DS@?3cCbFv#5B5g)C845R3$b5Hv=w%93n+kgU;zcP|J;jRzwE z)WnMsh2Q~MB^m@oq8Cw&2$2XV@!*4(;r*VO{XA3;+f!X#(=$`uy%WBqveUIyU;V#- zRekkVBEroqEqMdTTXH2pDb=Br8k;MIw7Y;(>ULlrFb9|dbORHC@kY|Kz;WOZa2VJF zd?zBOQ^l7C0ZOU6fu+C_Uk+eYz7_& zS{op_Ezq$!&>2_WRp5ox2>=xEx&z2iP}`Gz^iQMgDZs};8FA&WN{xWP`1c%z{{lSZ z+i(kxhk^YL9P82|;0;Hi@4NbT3M7mI-gLBvm0k%zd-=G&_f%GfTX6L`e9?dx0)SaD zg`5W#=4E^c-y-yIYO9>yh6n)0VIS(Qjs)Wk5^R<9JJ8t(0c&DBeQwR(fweEDE4*D7 z0l;*iU{Sa_Z(|*p-?88=0Qc8Jz}FUK`?B=*br8@F9I)WtUK0VpY>Sc-8I1EDNSGTl zJX*2GDf!Hz;KRI)tAT`#7W{*@w2{@IQmPC1o61#L3794#M`!|3N_7JVsoaPu0{4o@ z@o-w(PE9}2CFx+Z7!M#KhtL}{VQE8;6cu74V2LH}0}r^e78~ZL&4`LF5Nu$cqLP^7 z=%!QjjWA77N{s^k0qzV%mw-D&Zwy*qM9Z^}61yCfo)Z+v-jf?JJzf?iR^IknW% z*Xjh#sqk;;^ZT+Cfw3ZT-9>RQO!Wf`gQ#7=hAP8aYUxAy+JQ?{4uc0pr02$$!PX#A zrl(e8d87uQ$70WxxKRM~%?wj))8l3l8}>ut50e1f)Nz3&u32QEC2mjn!}I~S0sm1+ z%n4wUG}(&Ku^gTtBIn8x6)iwS&Ym4@AnFqPctagx-HD(RED$m}sVrv;Yw~0eophw*aryRS)^da-yw!s~9`N zKNI z+fWB)+ZCPyjyr|b6t1=?6re9}V=Y+wW3ayE%vVKeCwfoNRC*0ql(*3#oQttvkT?u< z)J;I3ycc~V#8iG^B#;mof6;>P3XXdLi3tb_8Hj0K*MOxt8s8sN^8zeOq@#wyR>c$~ z=xeRFW@vaj_Uk+4@M>aRH5RHx6PQTPzB0q9P_@M-9W z6RxYWiEF-Y0s=4tF^;huO2DVU6F^%7MvnrX1h(M5EPW+upZ7vQ0InB(4=)Z>Ih+hy z#WM63RqRYM=-WL(+KoZpYWfe8z}v@#MjuRc0xN)*N&e8Ggx;t)#qhyP2YT&(56K^1 z6o9qB3K2BTEVA*IWp#92BpP18nHrIu| zkhBE-v?gh@Hl+mYMepQn5|NXB5DrTg4=2Z?ucgdEpUrmzccC9nsvw!5A5Q*@c4|j} z?}2Z{*u(RVFKq&xg$K`OMdVU$81f`wB*;zwdi;NiiK$R6J&G>?0000N&PEETsKC?3F~p+x>SS-e1_d6M`xTzT z268>KUoFeKz`$shs_bdLey*pzV_MtuT)nSF%$}Y>GNRsGlY&^!zTsA2GB227^So~k zx9EG3cL!MK7@L`7uo{27upo1%^(Es S(sBuCH-o3EpUXO@geCxx1x&*L literal 0 HcmV?d00001 diff --git a/resources/img/x-32.png b/resources/img/x-32.png new file mode 100644 index 0000000000000000000000000000000000000000..785d7befb5343bc41d835d7516845ad133dbdd3b GIT binary patch literal 331 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE1|*BCs=fdz&H|6fVg?4`^&rfc{H*UVP*AeO zHKHUqKdq!Zu_%?Hyu4g5GcUV1Ik6yBFTW^#_B$IXprRX|E{-7)t#79}ay1(WxZd}8 zKRI{7ju)Y}NlS10nFnheJ(8kyNKyg zV3`*}dV7}cJ}6hQ@qpX`?)5?*jZqJ-GIh$ns|vouBrTM;W7RJEqyEhMvzhePEty}% z^5ss8Wk9!!@t)8}0u|jy5)0n@=&gAFeul7&N0X<@TW*y_sVyP>5y{)F-mk3S_|toU Zn}NyWzp!@RC!il0JYD@<);T3K0RZc!e%}B9 literal 0 HcmV?d00001 diff --git a/resources/img/x-64.png b/resources/img/x-64.png new file mode 100644 index 0000000000000000000000000000000000000000..37dce1ab667d77e328cbca34ef17ff6b429a6c16 GIT binary patch literal 655 zcmV;A0&x9_P)W z^#LFA(79r+0f&~&6j(av#u4)c$=6u=n!gpkq$B7LfhYdbPkttF{K$* + + diff --git a/resources/svg/circledots.svg b/resources/svg/circledots.svg new file mode 100644 index 0000000..6275a98 --- /dev/null +++ b/resources/svg/circledots.svg @@ -0,0 +1,4 @@ + + + + diff --git a/resources/svg/x.svg b/resources/svg/x.svg new file mode 100644 index 0000000..b209221 --- /dev/null +++ b/resources/svg/x.svg @@ -0,0 +1,3 @@ + + + From 9a4b1ce2390974bcebd891131b048a795ed5efb8 Mon Sep 17 00:00:00 2001 From: Jordan Wages Date: Tue, 8 Jul 2025 00:40:40 -0500 Subject: [PATCH 09/57] Update icons and options UI --- AGENTS.md | 7 +++++ README.md | 2 ++ background.js | 61 +++++++++++++++++++++++++++++++++++++++----- manifest.json | 2 +- options/options.html | 50 ++++++++++++++++++++++++++++-------- 5 files changed, 103 insertions(+), 19 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index ab79c66..c51f40c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -66,3 +66,10 @@ text extracted from all text parts. properties. Any legacy `aiReasonCache` data is merged into `aiCache` the first time the add-on loads after an update. +### Icon Set Usage + +Toolbar and menu icons reside under `resources/img` and are provided in 16, 32 +and 64 pixel variants. When changing these icons, pass a dictionary mapping the +sizes to the paths in `browserAction.setIcon` or `messageDisplayAction.setIcon`. +Use `resources/svg/svg2img.ps1` to regenerate PNGs from the SVG sources. + diff --git a/README.md b/README.md index eff53b1..bdff14a 100644 --- a/README.md +++ b/README.md @@ -141,3 +141,5 @@ Sortana builds upon knowledge gained from open-source projects. In particular, how Thunderbird's WebExtension and experiment APIs can be extended. Their code provided invaluable guidance during development. +- Icons from [cc0-icons.jonh.eu](https://cc0-icons.jonh.eu/) are used under the CC0 license. + diff --git a/background.js b/background.js index 59bac43..2916c06 100644 --- a/background.js +++ b/background.js @@ -28,6 +28,25 @@ let altTextImages = false; let collapseWhitespace = false; let TurndownService = null; +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" + } +}; + function setIcon(path) { if (browser.browserAction) { browser.browserAction.setIcon({ path }); @@ -38,9 +57,9 @@ function setIcon(path) { } function updateActionIcon() { - let path = "resources/img/brain.png"; + let path = ICONS.logo; if (processing || queuedCount > 0) { - path = "resources/img/busy.png"; + path = ICONS.circledots; } setIcon(path); } @@ -201,7 +220,7 @@ async function applyAiRules(idsInput) { t.mean += delta / t.count; t.m2 += delta * (elapsed - t.mean); await storage.local.set({ classifyStats: t }); - showTransientIcon("resources/img/done.png"); + showTransientIcon(ICONS.circle); } catch (e) { processing = false; const elapsed = Date.now() - currentStart; @@ -215,7 +234,7 @@ async function applyAiRules(idsInput) { t.m2 += delta * (elapsed - t.mean); await storage.local.set({ classifyStats: t }); logger.aiLog("failed to apply AI rules", { level: 'error' }, e); - showTransientIcon("resources/img/error.png"); + showTransientIcon(ICONS.average); } }); } @@ -250,7 +269,7 @@ async function clearCacheForMessages(idsInput) { } if (keys.length) { await AiClassifier.removeCacheEntries(keys); - showTransientIcon("resources/img/done.png"); + showTransientIcon(ICONS.circle); } } @@ -340,33 +359,61 @@ 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" + } }); 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" + } }); 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" + } }); 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" + } }); browser.menus.create({ id: "view-ai-reason-list", title: "View Reasoning", contexts: ["message_list"], - icons: { "16": "resources/img/brain.png" } + icons: { + 16: "resources/img/clipboarddata-16.png", + 32: "resources/img/clipboarddata-32.png", + 64: "resources/img/clipboarddata-64.png" + } }); browser.menus.create({ id: "view-ai-reason-display", title: "View Reasoning", contexts: ["message_display_action"], - icons: { "16": "resources/img/brain.png" } + icons: { + 16: "resources/img/clipboarddata-16.png", + 32: "resources/img/clipboarddata-32.png", + 64: "resources/img/clipboarddata-64.png" + } }); browser.menus.onClicked.addListener(async (info, tab) => { diff --git a/manifest.json b/manifest.json index 36b94b0..d44fd1d 100644 --- a/manifest.json +++ b/manifest.json @@ -22,7 +22,7 @@ "default_icon": "resources/img/logo32.png" }, "message_display_action": { - "default_icon": "resources/img/brain.png", + "default_icon": "resources/img/logo.png", "default_title": "Details", "default_label": "Details", "default_popup": "details.html" diff --git a/options/options.html b/options/options.html index 186e2b6..0fc5cae 100644 --- a/options/options.html +++ b/options/options.html @@ -44,18 +44,25 @@
- +
+

+ + Settings +

@@ -88,8 +95,14 @@
- - + +
+ diff --git a/options/options.js b/options/options.js index ebd7497..3a09d37 100644 --- a/options/options.js +++ b/options/options.js @@ -21,7 +21,9 @@ document.addEventListener('DOMContentLoaded', async () => { 'aiCache', 'theme', 'showDebugTab', - 'lastPayload' + 'lastPayload', + 'lastFullText', + 'lastPromptText' ]); const tabButtons = document.querySelectorAll('#main-tabs li'); const tabs = document.querySelectorAll('.tab-content'); @@ -67,9 +69,17 @@ document.addEventListener('DOMContentLoaded', async () => { await applyTheme(themeSelect.value); const payloadDisplay = document.getElementById('payload-display'); + const diffDisplay = document.getElementById('diff-display'); if (defaults.lastPayload) { payloadDisplay.textContent = JSON.stringify(defaults.lastPayload, null, 2); } + if (defaults.lastFullText && defaults.lastPromptText && diff_match_patch) { + const dmp = new diff_match_patch(); + dmp.Diff_EditCost = 4; + const diffs = dmp.diff_main(defaults.lastFullText, defaults.lastPromptText); + dmp.diff_cleanupEfficiency(diffs); + diffDisplay.innerHTML = dmp.diff_prettyHtml(diffs); + } themeSelect.addEventListener('change', async () => { markDirty(); await applyTheme(themeSelect.value); From 841a697c69a9204bcaf7c22c30be8fd5259837f5 Mon Sep 17 00:00:00 2001 From: Jordan Wages Date: Sat, 19 Jul 2025 19:46:44 -0500 Subject: [PATCH 54/57] Update debug tab with live refresh --- README.md | 2 +- options/options.js | 37 +++++++++++++++++++++++++++++++++---- 2 files changed, 34 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 64fbf26..82649d0 100644 --- a/README.md +++ b/README.md @@ -16,7 +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. -- **Debug tab** – view the last request payload sent to the AI service. +- **Debug tab** – view the last request payload and message diff with live updates. - **Light/Dark themes** – automatically match Thunderbird's appearance with optional manual override. - **Automatic rules** – create rules that tag, move, copy, forward, reply, delete, archive, mark read/unread or flag/unflag messages based on AI classification. Rules can optionally apply only to unread messages and can ignore messages outside a chosen age range. - **Rule ordering** – drag rules to prioritize them and optionally stop processing after a match. diff --git a/options/options.js b/options/options.js index 3a09d37..3402dcf 100644 --- a/options/options.js +++ b/options/options.js @@ -70,13 +70,18 @@ document.addEventListener('DOMContentLoaded', async () => { await applyTheme(themeSelect.value); const payloadDisplay = document.getElementById('payload-display'); const diffDisplay = document.getElementById('diff-display'); - if (defaults.lastPayload) { - payloadDisplay.textContent = JSON.stringify(defaults.lastPayload, null, 2); + + let lastFullText = defaults.lastFullText || ''; + let lastPromptText = defaults.lastPromptText || ''; + let lastPayload = defaults.lastPayload ? JSON.stringify(defaults.lastPayload, null, 2) : ''; + + if (lastPayload) { + payloadDisplay.textContent = lastPayload; } - if (defaults.lastFullText && defaults.lastPromptText && diff_match_patch) { + if (lastFullText && lastPromptText && diff_match_patch) { const dmp = new diff_match_patch(); dmp.Diff_EditCost = 4; - const diffs = dmp.diff_main(defaults.lastFullText, defaults.lastPromptText); + const diffs = dmp.diff_main(lastFullText, lastPromptText); dmp.diff_cleanupEfficiency(diffs); diffDisplay.innerHTML = dmp.diff_prettyHtml(diffs); } @@ -729,6 +734,30 @@ document.addEventListener('DOMContentLoaded', async () => { } catch { cacheCountEl.textContent = '?'; } + + try { + if (debugTabToggle.checked) { + const latest = await storage.local.get(['lastPayload', 'lastFullText', 'lastPromptText']); + const payloadStr = latest.lastPayload ? JSON.stringify(latest.lastPayload, null, 2) : ''; + if (payloadStr !== lastPayload) { + lastPayload = payloadStr; + payloadDisplay.textContent = payloadStr; + } + if (latest.lastFullText !== lastFullText || latest.lastPromptText !== lastPromptText) { + lastFullText = latest.lastFullText || ''; + lastPromptText = latest.lastPromptText || ''; + if (lastFullText && lastPromptText && diff_match_patch) { + const dmp = new diff_match_patch(); + dmp.Diff_EditCost = 4; + const diffs = dmp.diff_main(lastFullText, lastPromptText); + dmp.diff_cleanupEfficiency(diffs); + diffDisplay.innerHTML = dmp.diff_prettyHtml(diffs); + } else { + diffDisplay.innerHTML = ''; + } + } + } + } catch {} } refreshMaintenance(); From bcac4ad01709c85002e2f8123406bacffcf1d4cd Mon Sep 17 00:00:00 2001 From: Jordan Wages Date: Sat, 19 Jul 2025 21:58:37 -0500 Subject: [PATCH 55/57] Improve debug tab layout --- options/options.html | 15 +++++++++------ options/options.js | 22 ++++++++++++++++++++-- 2 files changed, 29 insertions(+), 8 deletions(-) diff --git a/options/options.html b/options/options.html index 3618a20..ddc5ee0 100644 --- a/options/options.html +++ b/options/options.html @@ -154,6 +154,11 @@ Aggressive token reduction
+
+ +
@@ -220,11 +225,6 @@
-
- -
@@ -290,7 +290,10 @@ Debug

-                
+ diff --git a/options/options.js b/options/options.js index 3402dcf..d881d6a 100644 --- a/options/options.js +++ b/options/options.js @@ -70,6 +70,7 @@ document.addEventListener('DOMContentLoaded', async () => { await applyTheme(themeSelect.value); const payloadDisplay = document.getElementById('payload-display'); const diffDisplay = document.getElementById('diff-display'); + const diffContainer = document.getElementById('diff-container'); let lastFullText = defaults.lastFullText || ''; let lastPromptText = defaults.lastPromptText || ''; @@ -83,7 +84,16 @@ document.addEventListener('DOMContentLoaded', async () => { dmp.Diff_EditCost = 4; const diffs = dmp.diff_main(lastFullText, lastPromptText); dmp.diff_cleanupEfficiency(diffs); - diffDisplay.innerHTML = dmp.diff_prettyHtml(diffs); + const hasDiff = diffs.some(d => d[0] !== 0); + if (hasDiff) { + diffDisplay.innerHTML = dmp.diff_prettyHtml(diffs); + diffContainer.classList.remove('is-hidden'); + } else { + diffDisplay.innerHTML = ''; + diffContainer.classList.add('is-hidden'); + } + } else { + diffContainer.classList.add('is-hidden'); } themeSelect.addEventListener('change', async () => { markDirty(); @@ -751,9 +761,17 @@ document.addEventListener('DOMContentLoaded', async () => { dmp.Diff_EditCost = 4; const diffs = dmp.diff_main(lastFullText, lastPromptText); dmp.diff_cleanupEfficiency(diffs); - diffDisplay.innerHTML = dmp.diff_prettyHtml(diffs); + const hasDiff = diffs.some(d => d[0] !== 0); + if (hasDiff) { + diffDisplay.innerHTML = dmp.diff_prettyHtml(diffs); + diffContainer.classList.remove('is-hidden'); + } else { + diffDisplay.innerHTML = ''; + diffContainer.classList.add('is-hidden'); + } } else { diffDisplay.innerHTML = ''; + diffContainer.classList.add('is-hidden'); } } } From 9cad2674e3841fbc40d237f7233fbd6784f61760 Mon Sep 17 00:00:00 2001 From: Jordan Wages Date: Sat, 19 Jul 2025 22:44:04 -0500 Subject: [PATCH 56/57] Capture raw message text for debug --- README.md | 2 +- background.js | 31 ++++++++++++++++++++++++++----- 2 files changed, 27 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 82649d0..57a0285 100644 --- a/README.md +++ b/README.md @@ -16,7 +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. -- **Debug tab** – view the last request payload and message diff with live updates. +- **Debug tab** – view the last request payload and a diff between the unaltered message text and the final prompt. - **Light/Dark themes** – automatically match Thunderbird's appearance with optional manual override. - **Automatic rules** – create rules that tag, move, copy, forward, reply, delete, archive, mark read/unread or flag/unflag messages based on AI classification. Rules can optionally apply only to unread messages and can ignore messages outside a chosen age range. - **Rule ordering** – drag rules to prioritize them and optionally stop processing after a match. diff --git a/background.js b/background.js index 58a5de5..fc585ff 100644 --- a/background.js +++ b/background.js @@ -210,17 +210,38 @@ function collectText(part, bodyParts, attachments) { } } -function buildEmailText(full) { +function collectRawText(part, bodyParts, attachments) { + if (part.parts && part.parts.length) { + for (const p of part.parts) collectRawText(p, bodyParts, attachments); + return; + } + const ct = (part.contentType || "text/plain").toLowerCase(); + const cd = (part.headers?.["content-disposition"]?.[0] || "").toLowerCase(); + const body = String(part.body || ""); + if (cd.includes("attachment") || !ct.startsWith("text/")) { + const nameMatch = /filename\s*=\s*"?([^";]+)/i.exec(cd) || /name\s*=\s*"?([^";]+)/i.exec(part.headers?.["content-type"]?.[0] || ""); + const name = nameMatch ? nameMatch[1] : ""; + attachments.push(`${name} (${ct}, ${part.size || byteSize(body)} bytes)`); + } else if (ct.startsWith("text/html")) { + const doc = new DOMParser().parseFromString(body, 'text/html'); + bodyParts.push(doc.body.textContent || ""); + } else { + bodyParts.push(body); + } +} + +function buildEmailText(full, applyTransforms = true) { const bodyParts = []; const attachments = []; - collectText(full, bodyParts, attachments); + const collect = applyTransforms ? collectText : collectRawText; + collect(full, bodyParts, attachments); const headers = Object.entries(full.headers || {}) .map(([k, v]) => `${k}: ${v.join(' ')}`) .join('\n'); const attachInfo = `Attachments: ${attachments.length}` + (attachments.length ? "\n" + attachments.map(a => ` - ${a}`).join('\n') : ""); let combined = `${headers}\n${attachInfo}\n\n${bodyParts.join('\n')}`.trim(); - if (tokenReduction) { + if (applyTransforms && tokenReduction) { const seen = new Set(); combined = combined.split('\n').filter(l => { if (seen.has(l)) return false; @@ -228,7 +249,7 @@ function buildEmailText(full) { return true; }).join('\n'); } - return sanitizeString(combined); + return applyTransforms ? sanitizeString(combined) : combined; } function updateTimingStats(elapsed) { @@ -262,8 +283,8 @@ async function processMessage(id) { updateActionIcon(); try { const full = await messenger.messages.getFull(id); + const originalText = buildEmailText(full, false); let text = buildEmailText(full); - const originalText = text; if (tokenReduction && maxTokens > 0) { const limit = Math.floor(maxTokens * 0.9); if (text.length > limit) { From c622c07c66a6936c4c8d3bb039b1f0bba4692384 Mon Sep 17 00:00:00 2001 From: Jordan Wages Date: Mon, 18 Aug 2025 19:39:35 -0500 Subject: [PATCH 57/57] Supporting v140+ After testing in Betterbird v140, updating manifest. --- manifest.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/manifest.json b/manifest.json index a3c9f7c..e7cb9d8 100644 --- a/manifest.json +++ b/manifest.json @@ -1,13 +1,13 @@ { "manifest_version": 2, "name": "Sortana", - "version": "2.1.2", + "version": "2.2.0", "default_locale": "en-US", "applications": { "gecko": { "id": "ai-filter@jordanwages", "strict_min_version": "128.0", - "strict_max_version": "139.*" + "strict_max_version": "140.*" } }, "icons": {