import { getActions, getContext } from '../lib/actions.js'; const ROOT = 'ask-chatgpt'; const SIMPLE = 'ask-chatgpt-selection'; const PREFIX = 'ask-chatgpt:'; const CONTEXTS = ['selection', 'link', 'page']; const MENU_KEYS = ['contextMenuMode', 'showContextMenu', 'showTranslate', 'translationMode', 'translationLanguage']; export function initializeMenus({ browser, load, onAction }) { let queue = Promise.resolve(); let mode = 'off'; let children = []; let menuInstance = 0; const enqueue = task => { queue = queue.catch(() => {}).then(task); return queue; }; const create = properties => new Promise((resolve, reject) => { browser.menus.create(properties, () => { const error = browser.runtime.lastError; if (error) reject(new Error(error.message)); else resolve(); }); }); function rebuild() { return enqueue(async () => { const settings = await load(); await browser.menus.removeAll(); children = []; mode = settings.contextMenuMode; if (mode === 'off') return; if (mode === 'simple') { await create({ id: SIMPLE, title: 'Ask ChatGPT', contexts: ['selection'] }); return; } await create({ id: ROOT, title: 'Ask ChatGPT', contexts: CONTEXTS }); for (const kind of ['selection', 'link', 'page']) { for (const action of getActions({ kind }, settings)) { const id = PREFIX + action.id; await create({ id, parentId: ROOT, title: action.title, contexts: CONTEXTS, visible: false }); children.push({ id, kind }); if (action.id === 'ask') { const separator = PREFIX + 'separator'; await create({ id: separator, parentId: ROOT, type: 'separator', contexts: CONTEXTS, visible: false }); children.push({ id: separator, kind }); } } } }); } browser.storage.onChanged.addListener((changes, area) => { if (area === 'sync' && MENU_KEYS.some(key => Object.hasOwn(changes, key))) return rebuild().catch(console.error); }); browser.menus.onHidden.addListener(() => { menuInstance++; }); browser.menus.onShown.addListener(info => { const instance = ++menuInstance; // Context labels are available without host access; selectionText/pageUrl // are deliberately NOT required until the user invokes an item. const contexts = info.contexts ?? []; const kind = contexts.includes('selection') ? 'selection' : contexts.includes('link') ? 'link' : contexts.includes('page') && !info.editable && !contexts.some(c => ['image', 'video', 'audio'].includes(c)) ? 'page' : null; return enqueue(async () => { if (instance !== menuInstance || mode !== 'advanced') return; await Promise.all([ browser.menus.update(ROOT, { visible: kind !== null }), ...children.map(child => browser.menus.update(child.id, { visible: child.kind === kind })), ]); if (instance === menuInstance) await browser.menus.refresh(); }).catch(console.error); }); browser.menus.onClicked.addListener(async info => { try { const settings = await load(); const context = getContext(info); if (!context || settings.contextMenuMode === 'off') return; let actionId; if (settings.contextMenuMode === 'simple') { if (info.menuItemId !== SIMPLE || context.kind !== 'selection') return; actionId = 'ask'; } else { const allowed = getActions(context, settings); actionId = allowed.find(action => PREFIX + action.id === info.menuItemId)?.id; if (!actionId) return; } await onAction({ actionId, source: context.source, settings }); } catch { console.error('Could not run context menu action.'); } }); return rebuild(); }