diff --git a/main.js b/main.js index 1e94df2..2958f6c 100644 --- a/main.js +++ b/main.js @@ -1,4 +1,4 @@ -const { app, BrowserWindow, ipcMain, session, screen, shell, dialog } = require('electron'); +const { app, BrowserWindow, ipcMain, session, screen, shell, dialog, Menu, clipboard } = require('electron'); const { pathToFileURL } = require('url'); const fs = require('fs'); const path = require('path'); @@ -612,3 +612,79 @@ ipcMain.handle('show-open-file-dialog', async () => { return null; } }); + +// Helper to build and show a native context menu for a given webContents + params +function buildAndShowContextMenu(sender, params = {}) { + try { + const embedder = sender.hostWebContents || sender; + const template = []; + + template.push( + { label: 'Back', enabled: sender.canGoBack?.(), click: () => { try { sender.goBack(); } catch {} } }, + { label: 'Forward', enabled: sender.canGoForward?.(), click: () => { try { sender.goForward(); } catch {} } }, + { label: 'Reload', click: () => { try { sender.reload(); } catch {} } }, + { type: 'separator' } + ); + + // Link actions + const linkURL = params.linkURL && params.linkURL.startsWith('http') ? params.linkURL : undefined; + if (linkURL) { + template.push( + { label: 'Open Link in New Tab', click: () => embedder.send('context-menu-command', { cmd: 'open-link-new-tab', url: linkURL }) }, + { label: 'Open Link Externally', click: () => shell.openExternal(linkURL).catch(()=>{}) }, + { label: 'Copy Link Address', click: () => clipboard.writeText(linkURL) }, + { type: 'separator' } + ); + } + + // Image actions + const imageURL = (params.mediaType === 'image' && params.srcURL) ? params.srcURL : (params.imgURL || undefined); + if (imageURL) { + template.push( + { label: 'Open Image in New Tab', click: () => embedder.send('context-menu-command', { cmd: 'open-image-new-tab', url: imageURL }) }, + { label: 'Copy Image Address', click: () => clipboard.writeText(imageURL) }, + { type: 'separator' } + ); + } + + // Text / editable + if (params.isEditable) { + template.push( + { label: 'Undo', role: 'undo' }, + { label: 'Redo', role: 'redo' }, + { type: 'separator' }, + { label: 'Cut', role: 'cut' }, + { label: 'Copy', role: 'copy' }, + { label: 'Paste', role: 'paste' }, + { label: 'Select All', role: 'selectAll' }, + { type: 'separator' } + ); + } else if (params.selectionText) { + template.push( + { label: 'Copy', role: 'copy' }, + { label: 'Select All', role: 'selectAll' }, + { type: 'separator' } + ); + } + + template.push({ label: 'Inspect Element', click: () => { try { sender.inspectElement(params.x ?? params.clientX, params.y ?? params.clientY); } catch {} } }); + + const menu = Menu.buildFromTemplate(template); + const win = BrowserWindow.fromWebContents(embedder); + if (win) menu.popup({ window: win }); + } catch (err) { + console.error('Failed to build context menu:', err); + } +} + +// IPC trigger (legacy / renderer-requested) +ipcMain.handle('show-context-menu', (event, params = {}) => { + buildAndShowContextMenu(event.sender, params); +}); + +// Automatic native context menu for any webContents (windows + webviews) +app.on('web-contents-created', (event, contents) => { + contents.on('context-menu', (e, params) => { + buildAndShowContextMenu(contents, params); + }); +}); diff --git a/preload.js b/preload.js index bc93181..91d7b49 100644 --- a/preload.js +++ b/preload.js @@ -63,6 +63,13 @@ const electronAPI = { console.error('IPC openLocalFile error:', err); return null; } + }, + showContextMenu: (params) => { + try { + return ipcRenderer.invoke('show-context-menu', params); + } catch (err) { + console.error('IPC showContextMenu error:', err); + } } }; @@ -105,4 +112,9 @@ contextBridge.exposeInMainWorld('bookmarksAPI', bookmarksAPI); // Minimal about API for settings page contextBridge.exposeInMainWorld('aboutAPI', { getInfo: () => ipcRenderer.invoke('get-about-info') +}); + +// Relay context-menu commands from main to active renderer context (open new tabs etc.) +ipcRenderer.on('context-menu-command', (event, payload) => { + window.dispatchEvent(new CustomEvent('nebula-context-command', { detail: payload })); }); \ No newline at end of file diff --git a/renderer/script.js b/renderer/script.js index 7301a93..31e6f9b 100644 --- a/renderer/script.js +++ b/renderer/script.js @@ -36,9 +36,7 @@ const urlBox = document.getElementById('url'); const tabBarEl = document.getElementById('tab-bar'); const webviewsEl = document.getElementById('webviews'); const menuPopup = document.getElementById('menu-popup'); -const contextMenu = document.getElementById('context-menu'); -const menuItems = contextMenu ? contextMenu.querySelectorAll('li') : []; -let lastContextPos = { x: 0, y: 0 }; +// (Removed old custom HTML context menu in favor of native Electron menu) // Select all text on focus and prevent mouseup from deselecting urlBox.addEventListener('focus', () => { @@ -997,55 +995,33 @@ try { console.warn('fs or remote modules unavailable in renderer:', err); } -// 4) unify context-menu wiring -function showContextMenu(x,y) { - if (!contextMenu) return; - lastContextPos = { x, y }; - contextMenu.style.top = `${y}px`; - contextMenu.style.left = `${x}px`; - contextMenu.classList.add('visible'); -} -document.addEventListener('contextmenu', e => { - if (e.target.tagName==='WEBVIEW' || e.composedPath().some(el=>el.id==='webviews')) { - e.preventDefault(); - showContextMenu(e.clientX, e.clientY); +// Native context menu: delegate to main via preload API +document.addEventListener('contextmenu', (e) => { + // Determine if inside a webview or general renderer area + const inWebviewArea = e.target.tagName === 'WEBVIEW' || e.composedPath().some(el => el.id === 'webviews'); + if (!inWebviewArea) return; // Let default OS menu appear in text inputs etc. if desired + e.preventDefault(); + + // Try to extract link/image/selection info (limited for , better done inside page but sandboxed) + const selection = window.getSelection()?.toString() || ''; + window.electronAPI?.showContextMenu({ + clientX: e.clientX, + clientY: e.clientY, + selectionText: selection, + isEditable: false + }); +}); + +// Handle commands from main process triggered by context menu +window.addEventListener('nebula-context-command', (e) => { + const { cmd, url } = e.detail || {}; + if (!cmd) return; + switch (cmd) { + case 'open-link-new-tab': + if (url) createTab(url); + break; + case 'open-image-new-tab': + if (url) createTab(url); + break; } }); -document.addEventListener('click', ()=> contextMenu && contextMenu.classList.remove('visible')); -if (remote && fs) { - menuItems.forEach(item => item.addEventListener('click', async evt => { - const action = item.dataset.action; - const win = remote.getCurrentWindow(); - - switch (action) { - case 'save-page': { - const { canceled, filePath } = await remote.dialog.showSaveDialog(win, { defaultPath: 'page.html' }); - if (!canceled && filePath) win.webContents.savePage(filePath, 'HTMLComplete'); - break; - } - case 'select-all': - document.execCommand('selectAll'); - break; - case 'screenshot': { - const image = await win.webContents.capturePage(); - const { canceled, filePath } = await remote.dialog.showSaveDialog(win, { defaultPath: 'screenshot.png' }); - if (!canceled && filePath) fs.writeFileSync(filePath, image.toPNG()); - break; - } - case 'view-source': { - const html = document.documentElement.outerHTML; - const { canceled, filePath } = await remote.dialog.showSaveDialog(win, { defaultPath: 'source.html' }); - if (!canceled && filePath) fs.writeFileSync(filePath, html); - break; - } - case 'inspect-accessibility': - win.webContents.inspectAccessibilityNode(lastContextPos.x, lastContextPos.y); - break; - case 'inspect-element': - win.webContents.inspectElement(lastContextPos.x, lastContextPos.y); - break; - } - - contextMenu.classList.remove('visible'); - })); -} diff --git a/site-history.json b/site-history.json index f5af1c9..4887cdb 100644 --- a/site-history.json +++ b/site-history.json @@ -1,4 +1,9 @@ [ + "https://www.google.com/search?sca_esv=8740a35e22b44344&q=google&source=lnms&fbs=AIIjpHzThbnmQ2WmOKxM311CRlKFRYIYkDZQGNzKWZOtgUvrU8IkX2D8ZljVyZBwLc67VO5Vh9BmSq-tJTelkfgGaeLOhsWoMcpPaMobUKT2lDeoy4baWd3FunAQvdgUkx-O2UqTNd3rElthg_q_RDuOd63_-9VEOzcZa8DOthTdfufpgCAS8atIRQu6ndbQbff19E3EbkrdgATyQCGbkaHPxI6YLnT-EcRkSXiWTOApoudKAVtFgl4&sa=X&ved=2ahUKEwiP8MW4nIyPAxXwr1YBHfDEE0wQ0pQJegQICRAB&biw=2544&bih=1251&dpr=1.5", + "https://www.google.com/search?sca_esv=8740a35e22b44344&udm=2&fbs=AIIjpHxU7SXXniUZfeShr2fp4giZ1Y6MJ25_tmWITc7uy4KIeuYzzFkfneXafNx6OMdA4MQRJc_t_TQjwHYrzlkIauOKj9nSuujpEIbB1x32lFLEvBmmX-p1UI3WlSFH86-EF1CpFR0tZjCgi5bM20K3xHOK3droXh0yMXraJ5han3x4rkl9Co5S6JKPNx1fHkXHoy-qehbRF1XGgIa6fKwyF5LNOJ-3xQ&q=google&sa=X&ved=2ahUKEwij2L2QnIyPAxVjsVYBHf8oASsQtKgLegQIHhAB&cshid=1755240501153677&biw=2544&bih=1251&dpr=1.5", + "https://www.google.com/search?q=google#cobssid=s", + "https://www.google.com/", + "https://www.google.com/search?q=google", "https://www.whatismybrowser.com/", "https://www.google.com/search?q=what%20is%20my%20browser" ] \ No newline at end of file