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