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:
parent
7348316409
commit
a4be6d0c4f
3 changed files with 136 additions and 16 deletions
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"manifest_version": 2,
|
"manifest_version": 2,
|
||||||
"name": "Archive.org Link Grabber",
|
"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.",
|
"description": "Filter and export archive.org /download links; copy or send to aria2 RPC.",
|
||||||
"applications": {
|
"applications": {
|
||||||
"gecko": {
|
"gecko": {
|
||||||
|
|
@ -36,8 +36,12 @@
|
||||||
},
|
},
|
||||||
"content_scripts": [
|
"content_scripts": [
|
||||||
{
|
{
|
||||||
"matches": ["https://archive.org/download/*"],
|
"matches": [
|
||||||
"js": ["src/content/collect.js"]
|
"https://archive.org/download/*"
|
||||||
|
],
|
||||||
|
"js": [
|
||||||
|
"src/content/collect.js"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,53 @@
|
||||||
// In MV2 background, aria2 helpers are exposed on globalThis by src/lib/aria2-bg.js
|
// In MV2 background, aria2 helpers are exposed on globalThis by src/lib/aria2-bg.js
|
||||||
|
|
||||||
const DYNAMIC_PREFIX = 'aolg-act-';
|
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 = [];
|
let dynamicMenuIds = [];
|
||||||
const dynamicActions = new Map(); // id -> { kind: 'all'|'type', type?: string }
|
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' };
|
if (!tabId) return { ok: false, error: 'No tabId' };
|
||||||
try {
|
try {
|
||||||
const items = await collectFromTab(tabId);
|
const items = await collectFromTab(tabId);
|
||||||
|
const s = getState(tabId);
|
||||||
|
s.itemsCount = (items || []).length;
|
||||||
|
await recomputeBadge(tabId);
|
||||||
return { ok: true, items };
|
return { ok: true, items };
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
return { ok: false, error: String(err && err.message || 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) {
|
async function collectFromTab(tabId) {
|
||||||
|
|
@ -172,13 +232,20 @@ browser.contextMenus.onClicked.addListener(async (info, tab) => {
|
||||||
try {
|
try {
|
||||||
const items = await collectFromTab(tab.id);
|
const items = await collectFromTab(tab.id);
|
||||||
const count = (items || []).length;
|
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({
|
await browser.storage.local.set({
|
||||||
lastCollected: { tabId: tab.id, url: tab.url, time: Date.now(), count },
|
lastCollected: { tabId: tab.id, url: tab.url, time: Date.now(), count },
|
||||||
lastItems: items
|
lastItems: items
|
||||||
});
|
});
|
||||||
try { await browser.browserAction.setBadgeBackgroundColor({ color: '#3b82f6' }); } catch (e) {}
|
await setBadgeForTab(tab.id, {
|
||||||
try { await browser.browserAction.setBadgeText({ text: count ? String(count) : '' }); } catch (e) {}
|
text: count ? String(count) : '',
|
||||||
try { await browser.browserAction.setTitle({ title: count ? `Collected ${count} link(s)` : 'No links collected' }); } catch (e) {}
|
color: '#3b82f6',
|
||||||
|
title: count ? `Collected ${count} link(s)` : 'No links collected'
|
||||||
|
});
|
||||||
console.debug('Collected links from page:', { count });
|
console.debug('Collected links from page:', { count });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn('Context menu collection failed:', e);
|
console.warn('Context menu collection failed:', e);
|
||||||
|
|
@ -204,18 +271,60 @@ browser.contextMenus.onClicked.addListener(async (info, tab) => {
|
||||||
|
|
||||||
if (defaultAction === 'copy') {
|
if (defaultAction === 'copy') {
|
||||||
try { await navigator.clipboard.writeText(uris.join('\n')); } catch (e) { console.warn('clipboard write failed', e); }
|
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) {}
|
await setBadgeForTab(tab.id, {
|
||||||
try { await browser.browserAction.setBadgeText({ text: String(count) }); } catch (e) {}
|
text: String(count),
|
||||||
try { await browser.browserAction.setTitle({ title: `Copied ${count} link(s)` }); } catch (e) {}
|
color: '#10b981',
|
||||||
|
title: `Copied ${count} link(s)`
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
const { endpoint, secret } = await getAria2();
|
const { endpoint, secret } = await getAria2();
|
||||||
if (!endpoint) { console.warn('aria2 endpoint not set'); return; }
|
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 globalThis.addUrisBatch({ endpoint, secret, uris }); } catch (e) { console.warn('aria2 send failed', e); }
|
||||||
try { await browser.browserAction.setBadgeBackgroundColor({ color: '#3b82f6' }); } catch (e) {}
|
await setBadgeForTab(tab.id, {
|
||||||
try { await browser.browserAction.setBadgeText({ text: String(count) }); } catch (e) {}
|
text: String(count),
|
||||||
try { await browser.browserAction.setTitle({ title: `Sent ${count} link(s) to aria2` }); } catch (e) {}
|
color: '#3b82f6',
|
||||||
|
title: `Sent ${count} link(s) to aria2`
|
||||||
|
});
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn('Quick action failed:', 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);
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -57,7 +57,7 @@ async function refresh() {
|
||||||
else throw new Error(res?.error || e?.message || 'collect failed');
|
else throw new Error(res?.error || e?.message || 'collect failed');
|
||||||
}
|
}
|
||||||
allItems = items || [];
|
allItems = items || [];
|
||||||
updateCount();
|
await updateCount();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
els.status.textContent = String(e?.message || e);
|
els.status.textContent = String(e?.message || e);
|
||||||
}
|
}
|
||||||
|
|
@ -67,9 +67,16 @@ function filtered() {
|
||||||
return applyFilters(allItems, getFilters());
|
return applyFilters(allItems, getFilters());
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateCount() {
|
async function updateCount() {
|
||||||
els.count.textContent = String(filtered().length);
|
const count = filtered().length;
|
||||||
|
els.count.textContent = String(count);
|
||||||
renderPreview();
|
renderPreview();
|
||||||
|
try {
|
||||||
|
const tabId = await getActiveTabId();
|
||||||
|
if (tabId) {
|
||||||
|
await browser.runtime.sendMessage({ type: 'popup.filteredCount', tabId, count });
|
||||||
|
}
|
||||||
|
} catch (_) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function copyLinks() {
|
async function copyLinks() {
|
||||||
|
|
@ -118,7 +125,7 @@ function wire() {
|
||||||
[
|
[
|
||||||
els.fType, els.fTypeRe, els.fName, els.fNameRe, els.fSizeMin, els.fSizeMax,
|
els.fType, els.fTypeRe, els.fName, els.fNameRe, els.fSizeMin, els.fSizeMax,
|
||||||
els.fDateFrom, els.fDateTo, els.fCase
|
els.fDateFrom, els.fDateTo, els.fCase
|
||||||
].forEach(el => el.addEventListener('input', updateCount));
|
].forEach(el => el.addEventListener('input', () => { updateCount(); }));
|
||||||
}
|
}
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', async () => {
|
document.addEventListener('DOMContentLoaded', async () => {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue