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,
 | 
			
		||||
  "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"
 | 
			
		||||
      ]
 | 
			
		||||
    }
 | 
			
		||||
  ]
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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);
 | 
			
		||||
});
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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 () => {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue