diff --git a/src/background/index.js b/src/background/index.js index 8164672..245963a 100644 --- a/src/background/index.js +++ b/src/background/index.js @@ -1,5 +1,102 @@ // In MV2 background, aria2 helpers are exposed on globalThis by src/lib/aria2-bg.js +const DYNAMIC_PREFIX = 'aolg-act-'; +let dynamicMenuIds = []; +const dynamicActions = new Map(); // id -> { kind: 'all'|'type', type?: string } + +function parseIdentifierFromUrl(url) { + try { + const u = new URL(url); + const parts = u.pathname.split('/').filter(Boolean); + const idx = parts.indexOf('download'); + if (idx >= 0 && parts[idx + 1]) return parts[idx + 1]; + } catch (_) {} + return ''; +} + +function isMeta(item, identifier) { + const name = String(item?.name || ''); + if (!name) return false; + if (name === '__ia_thumb.jpg') return true; + if (identifier && name.startsWith(identifier + '_')) return true; + return false; +} + +async function getPrefs() { + const { prefs } = await browser.storage.local.get('prefs'); + return { + defaultAction: prefs?.defaultAction || 'download', + allIncludeMeta: !!prefs?.allIncludeMeta, + }; +} + +async function getAria2() { + const { aria2 } = await browser.storage.local.get('aria2'); + return { endpoint: aria2?.endpoint || '', secret: aria2?.secret || '' }; +} + +function computeTopTypes(items, identifier, limit = 4) { + const counts = new Map(); + for (const it of items || []) { + if (isMeta(it, identifier)) continue; + const t = (it.type || '').toLowerCase(); + if (!t) continue; + counts.set(t, (counts.get(t) || 0) + 1); + } + return Array.from(counts.entries()) + .sort((a, b) => b[1] - a[1]) + .slice(0, limit) + .map(([type, count]) => ({ type, count })); +} + +async function rebuildContextSubmenu(tab) { + if (!tab?.id) return; + // Clear previous dynamic children + for (const id of dynamicMenuIds) { + try { await browser.contextMenus.remove(id); } catch (_) {} + dynamicActions.delete(id); + } + dynamicMenuIds = []; + + let items = []; + try { items = await collectFromTab(tab.id); } catch (_) { items = []; } + const identifier = parseIdentifierFromUrl(tab.url || ''); + const { defaultAction, allIncludeMeta } = await getPrefs(); + const verb = defaultAction === 'copy' ? 'Copy' : 'Download'; + + const top = computeTopTypes(items, identifier, 4); + + // All action + const allId = DYNAMIC_PREFIX + 'all'; + try { + await browser.contextMenus.create({ + id: allId, + title: `${verb} All`, + parentId: 'aolg-collect', + contexts: ['page'] + }); + dynamicMenuIds.push(allId); + dynamicActions.set(allId, { kind: 'all' }); + } catch (_) {} + + // Type-specific actions + for (const { type, count } of top) { + const id = `${DYNAMIC_PREFIX}type:${type}`; + try { + await browser.contextMenus.create({ + id, + title: `${verb} All ${type} (${count})`, + parentId: 'aolg-collect', + contexts: ['page'] + }); + dynamicMenuIds.push(id); + dynamicActions.set(id, { kind: 'type', type }); + } catch (_) {} + } + + try { await browser.contextMenus.refresh(); } catch (_) {} +} + browser.runtime.onMessage.addListener(async (msg, sender) => { if (!msg || !msg.type) return; @@ -56,7 +153,7 @@ browser.runtime.onInstalled.addListener(() => { try { browser.contextMenus.create({ id: 'aolg-collect', - title: 'Collect archive.org links', + title: 'Collect links…', contexts: ['page'], documentUrlPatterns: ['https://archive.org/download/*'] }); @@ -65,6 +162,11 @@ browser.runtime.onInstalled.addListener(() => { } }); +browser.contextMenus.onShown.addListener(async (info, tab) => { + if (!tab?.url || !/^https:\/\/archive\.org\/download\//.test(tab.url)) return; + await rebuildContextSubmenu(tab); +}); + browser.contextMenus.onClicked.addListener(async (info, tab) => { if (info.menuItemId !== 'aolg-collect' || !tab?.id) return; try { @@ -82,3 +184,38 @@ browser.contextMenus.onClicked.addListener(async (info, tab) => { console.warn('Context menu collection failed:', e); } }); + +browser.contextMenus.onClicked.addListener(async (info, tab) => { + const action = dynamicActions.get(String(info.menuItemId)); + if (!action || !tab?.id) return; + try { + const { defaultAction, allIncludeMeta } = await getPrefs(); + const identifier = parseIdentifierFromUrl(tab.url || ''); + const items = await collectFromTab(tab.id); + let selected = items || []; + if (action.kind === 'type') { + selected = selected.filter(it => (it.type || '').toLowerCase() === String(action.type || '').toLowerCase()); + } else { + if (!allIncludeMeta) selected = selected.filter(it => !isMeta(it, identifier)); + } + const uris = selected.map(i => i.url); + const count = uris.length; + if (count === 0) return; + + 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) {} + } 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) {} + } + } catch (e) { + console.warn('Quick action failed:', e); + } +}); diff --git a/src/options/index.html b/src/options/index.html index 3b01b46..4c54fb7 100644 --- a/src/options/index.html +++ b/src/options/index.html @@ -27,8 +27,22 @@ + +
+

Quick Actions

+
+ +
+
+ +
+
- diff --git a/src/options/index.js b/src/options/index.js index 887a95a..b0be344 100644 --- a/src/options/index.js +++ b/src/options/index.js @@ -3,6 +3,8 @@ import { getVersion } from "../lib/aria2.js"; const els = { endpoint: document.getElementById('rpc-endpoint'), secret: document.getElementById('rpc-secret'), + defaultAction: document.getElementById('default-action'), + includeMeta: document.getElementById('include-meta'), btnSave: document.getElementById('btn-save'), btnReset: document.getElementById('btn-reset'), btnTest: document.getElementById('btn-test'), @@ -13,17 +15,28 @@ async function restore() { const { aria2 } = await browser.storage.local.get('aria2'); els.endpoint.value = aria2?.endpoint || 'http://localhost:6800/jsonrpc'; els.secret.value = aria2?.secret || ''; + + const { prefs } = await browser.storage.local.get('prefs'); + const defaultAction = prefs?.defaultAction || 'download'; + const includeMeta = !!prefs?.allIncludeMeta; + els.defaultAction.value = defaultAction; + els.includeMeta.checked = includeMeta; } async function save() { const endpoint = els.endpoint.value.trim(); const secret = els.secret.value.trim(); - await browser.storage.local.set({ aria2: { endpoint, secret } }); + const defaultAction = els.defaultAction.value; + const allIncludeMeta = !!els.includeMeta.checked; + await browser.storage.local.set({ + aria2: { endpoint, secret }, + prefs: { defaultAction, allIncludeMeta } + }); els.status.textContent = 'Saved.'; } async function reset() { - await browser.storage.local.remove('aria2'); + await browser.storage.local.remove(['aria2', 'prefs']); await restore(); els.status.textContent = 'Reset to defaults.'; }