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

This commit is contained in:
Jordan Wages 2025-08-22 23:28:33 -05:00
commit a4be6d0c4f
3 changed files with 136 additions and 16 deletions

View file

@ -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"
]
}
]
}

View file

@ -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);
});

View file

@ -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 () => {