feat(context-menu): add dynamic quick actions and options
- Parent menu now has submenu with "All" and top 4 file-type actions - Actions honor new Options: default action (download/copy) and include metadata in All - Build submenu on open by collecting links; exclude meta from top-types - Handle actions: copy to clipboard or send to aria2 using saved settings
This commit is contained in:
parent
92ec0a9a74
commit
9d5a3e4224
3 changed files with 168 additions and 4 deletions
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -27,8 +27,22 @@
|
|||
<output id="status"></output>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Quick Actions</h2>
|
||||
<div class="row">
|
||||
<label>Default Action
|
||||
<select id="default-action">
|
||||
<option value="download">Download via aria2 RPC</option>
|
||||
<option value="copy">Copy to clipboard</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
<div class="row">
|
||||
<label class="chk"><input type="checkbox" id="include-meta" /> Include metadata in “All”</label>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
<script type="module" src="index.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
|
|
|
|||
|
|
@ -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.';
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue