From a4be6d0c4f4d02dc1d376faf3981835f05dd3359 Mon Sep 17 00:00:00 2001 From: wagesj45 Date: Fri, 22 Aug 2025 23:28:33 -0500 Subject: [PATCH] feat(badge): make badge per-tab and reactive\n\n- Track per-tab counts (total + filtered)\n- Update on tab switch and URL change\n- Popup sends filtered count; background updates badge for tab\n- Default to total count on archive.org download pages\n- Scope quick-action badge updates to tabId --- manifest.json | 10 +++- src/background/index.js | 127 +++++++++++++++++++++++++++++++++++++--- src/popup/index.js | 15 +++-- 3 files changed, 136 insertions(+), 16 deletions(-) diff --git a/manifest.json b/manifest.json index 26327a8..49d8842 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "manifest_version": 2, "name": "Archive.org Link Grabber", - "version": "0.1.0", + "version": "0.1.1", "description": "Filter and export archive.org /download links; copy or send to aria2 RPC.", "applications": { "gecko": { @@ -36,8 +36,12 @@ }, "content_scripts": [ { - "matches": ["https://archive.org/download/*"], - "js": ["src/content/collect.js"] + "matches": [ + "https://archive.org/download/*" + ], + "js": [ + "src/content/collect.js" + ] } ] } diff --git a/src/background/index.js b/src/background/index.js index 245963a..0ee2986 100644 --- a/src/background/index.js +++ b/src/background/index.js @@ -1,6 +1,53 @@ // In MV2 background, aria2 helpers are exposed on globalThis by src/lib/aria2-bg.js const DYNAMIC_PREFIX = 'aolg-act-'; +const A_DOWNLOAD_RE = /^https:\/\/archive\.org\/download\//; + +// Per-tab state: { url, isArchive, itemsCount, filteredCount } +const tabState = new Map(); + +function getState(tabId) { + let s = tabState.get(tabId); + if (!s) { s = { url: '', isArchive: false, itemsCount: 0, filteredCount: null }; tabState.set(tabId, s); } + return s; +} + +async function setBadgeForTab(tabId, { text = '', color = '#3b82f6', title = '' } = {}) { + try { await browser.browserAction.setBadgeBackgroundColor({ color, tabId }); } catch (_) {} + try { await browser.browserAction.setBadgeText({ text, tabId }); } catch (_) {} + if (title) { try { await browser.browserAction.setTitle({ title, tabId }); } catch (_) {} } +} + +async function recomputeBadge(tabId) { + try { + const tab = await browser.tabs.get(tabId); + const s = getState(tabId); + s.url = tab.url || ''; + s.isArchive = !!(tab.url && A_DOWNLOAD_RE.test(tab.url)); + // Prefer filteredCount when present (user-adjusted in popup) + if (s.filteredCount != null) { + const count = s.filteredCount; + await setBadgeForTab(tabId, { + text: count ? String(count) : '', + color: '#6366f1', + title: count ? `Filtered ${count} link(s)` : 'No filtered links' + }); + return; + } + // Default for archive pages: show total items + if (s.isArchive) { + const count = s.itemsCount || 0; + await setBadgeForTab(tabId, { + text: count ? String(count) : '', + color: '#3b82f6', + title: count ? `Found ${count} link(s)` : 'No links found' + }); + } else { + // Non-archive pages: clear badge unless popup provides a filtered count + await setBadgeForTab(tabId, { text: '' }); + } + } catch (_) {} +} let dynamicMenuIds = []; const dynamicActions = new Map(); // id -> { kind: 'all'|'type', type?: string } @@ -118,11 +165,24 @@ browser.runtime.onMessage.addListener(async (msg, sender) => { if (!tabId) return { ok: false, error: 'No tabId' }; try { const items = await collectFromTab(tabId); + const s = getState(tabId); + s.itemsCount = (items || []).length; + await recomputeBadge(tabId); return { ok: true, items }; } catch (err) { return { ok: false, error: String(err && err.message || err) }; } } + + if (msg.type === 'popup.filteredCount') { + const tabId = msg.tabId || sender?.tab?.id; + if (!tabId) return; + const count = Number(msg.count || 0); + const s = getState(tabId); + s.filteredCount = count; + await recomputeBadge(tabId); + return { ok: true }; + } }); async function collectFromTab(tabId) { @@ -172,13 +232,20 @@ browser.contextMenus.onClicked.addListener(async (info, tab) => { try { const items = await collectFromTab(tab.id); const count = (items || []).length; + const s = getState(tab.id); + s.url = tab.url || ''; + s.isArchive = !!(tab.url && A_DOWNLOAD_RE.test(tab.url)); + s.itemsCount = count; + s.filteredCount = null; // reset to default after explicit collect await browser.storage.local.set({ lastCollected: { tabId: tab.id, url: tab.url, time: Date.now(), count }, lastItems: items }); - try { await browser.browserAction.setBadgeBackgroundColor({ color: '#3b82f6' }); } catch (e) {} - try { await browser.browserAction.setBadgeText({ text: count ? String(count) : '' }); } catch (e) {} - try { await browser.browserAction.setTitle({ title: count ? `Collected ${count} link(s)` : 'No links collected' }); } catch (e) {} + await setBadgeForTab(tab.id, { + text: count ? String(count) : '', + color: '#3b82f6', + title: count ? `Collected ${count} link(s)` : 'No links collected' + }); console.debug('Collected links from page:', { count }); } catch (e) { console.warn('Context menu collection failed:', e); @@ -204,18 +271,60 @@ browser.contextMenus.onClicked.addListener(async (info, tab) => { if (defaultAction === 'copy') { try { await navigator.clipboard.writeText(uris.join('\n')); } catch (e) { console.warn('clipboard write failed', e); } - try { await browser.browserAction.setBadgeBackgroundColor({ color: '#10b981' }); } catch (e) {} - try { await browser.browserAction.setBadgeText({ text: String(count) }); } catch (e) {} - try { await browser.browserAction.setTitle({ title: `Copied ${count} link(s)` }); } catch (e) {} + await setBadgeForTab(tab.id, { + text: String(count), + color: '#10b981', + title: `Copied ${count} link(s)` + }); } else { const { endpoint, secret } = await getAria2(); if (!endpoint) { console.warn('aria2 endpoint not set'); return; } try { await globalThis.addUrisBatch({ endpoint, secret, uris }); } catch (e) { console.warn('aria2 send failed', e); } - try { await browser.browserAction.setBadgeBackgroundColor({ color: '#3b82f6' }); } catch (e) {} - try { await browser.browserAction.setBadgeText({ text: String(count) }); } catch (e) {} - try { await browser.browserAction.setTitle({ title: `Sent ${count} link(s) to aria2` }); } catch (e) {} + await setBadgeForTab(tab.id, { + text: String(count), + color: '#3b82f6', + title: `Sent ${count} link(s) to aria2` + }); } } catch (e) { console.warn('Quick action failed:', e); } }); + +// React to tab activation and URL changes to refresh per-tab badges +browser.tabs.onActivated.addListener(async ({ tabId }) => { + await recomputeBadge(tabId); + try { + const tab = await browser.tabs.get(tabId); + if (tab?.url && A_DOWNLOAD_RE.test(tab.url)) { + const items = await collectFromTab(tabId); + const s = getState(tabId); + s.itemsCount = (items || []).length; + // Only update badge if no filtered count is set + if (s.filteredCount == null) await recomputeBadge(tabId); + } + } catch (_) {} +}); + +browser.tabs.onUpdated.addListener(async (tabId, changeInfo, tab) => { + if (changeInfo.status === 'complete' || changeInfo.url) { + const s = getState(tabId); + s.url = tab?.url || ''; + s.isArchive = !!(tab?.url && A_DOWNLOAD_RE.test(tab.url)); + // Reset filtered count on navigation; it reflects popup state for previous page + s.filteredCount = null; + if (s.isArchive) { + try { + const items = await collectFromTab(tabId); + s.itemsCount = (items || []).length; + } catch (_) { s.itemsCount = 0; } + } else { + s.itemsCount = 0; + } + await recomputeBadge(tabId); + } +}); + +browser.tabs.onRemoved.addListener((tabId) => { + tabState.delete(tabId); +}); diff --git a/src/popup/index.js b/src/popup/index.js index 78419e9..b5d793e 100644 --- a/src/popup/index.js +++ b/src/popup/index.js @@ -57,7 +57,7 @@ async function refresh() { else throw new Error(res?.error || e?.message || 'collect failed'); } allItems = items || []; - updateCount(); + await updateCount(); } catch (e) { els.status.textContent = String(e?.message || e); } @@ -67,9 +67,16 @@ function filtered() { return applyFilters(allItems, getFilters()); } -function updateCount() { - els.count.textContent = String(filtered().length); +async function updateCount() { + const count = filtered().length; + els.count.textContent = String(count); renderPreview(); + try { + const tabId = await getActiveTabId(); + if (tabId) { + await browser.runtime.sendMessage({ type: 'popup.filteredCount', tabId, count }); + } + } catch (_) {} } async function copyLinks() { @@ -118,7 +125,7 @@ function wire() { [ els.fType, els.fTypeRe, els.fName, els.fNameRe, els.fSizeMin, els.fSizeMax, els.fDateFrom, els.fDateTo, els.fCase - ].forEach(el => el.addEventListener('input', updateCount)); + ].forEach(el => el.addEventListener('input', () => { updateCount(); })); } document.addEventListener('DOMContentLoaded', async () => {