diff --git a/main.js b/main.js
index 5423ce1..74de7ad 100644
--- a/main.js
+++ b/main.js
@@ -127,7 +127,7 @@ function initializeSteamworks() {
// This is critical for Steam Input to recognize native controller support
initializeSteamworks();
-const { app, BrowserWindow, ipcMain, session, screen, shell, dialog, Menu, clipboard, webContents } = require('electron');
+const { app, BrowserWindow, BrowserView, ipcMain, session, screen, shell, dialog, Menu, clipboard, webContents } = require('electron');
// Cleanup Steam callback pump on exit
app.once('before-quit', () => {
@@ -157,6 +157,308 @@ const gpuFallback = new GPUFallback();
const gpuConfig = new GPUConfig();
const pluginManager = new PluginManager();
+// =============================================================================
+// DESKTOP MODE: BrowserView tab management
+// =============================================================================
+const desktopViewStateByWindowId = new Map();
+const desktopViewByWebContentsId = new Map();
+const menuPopupByWindowId = new Map();
+const MENU_POPUP_SIZE = { width: 240, height: 240 };
+
+const SCROLL_NORMALIZATION_CSS = `
+ *, *::before, *::after { scroll-behavior: auto !important; }
+ html, body { scroll-behavior: auto !important; }
+`;
+
+const SCROLL_NORMALIZATION_JS = `
+(function() {
+ if (window.__nebulaScrollNormalized) return;
+ window.__nebulaScrollNormalized = true;
+ const SCROLL_SPEED = 100;
+ document.addEventListener('wheel', function(e) {
+ if (e.ctrlKey || e.metaKey || e.altKey) return;
+ let target = e.target;
+ let scrollable = null;
+ while (target && target !== document.body && target !== document.documentElement) {
+ const style = window.getComputedStyle(target);
+ const overflowY = style.overflowY;
+ const overflowX = style.overflowX;
+ if ((overflowY === 'auto' || overflowY === 'scroll') && target.scrollHeight > target.clientHeight) { scrollable = target; break; }
+ if ((overflowX === 'auto' || overflowX === 'scroll') && target.scrollWidth > target.clientWidth && e.shiftKey) { scrollable = target; break; }
+ target = target.parentElement;
+ }
+ if (!scrollable) scrollable = document.scrollingElement || document.documentElement || document.body;
+ let deltaY = e.deltaY;
+ let deltaX = e.deltaX;
+ if (e.deltaMode === 1) {
+ deltaY *= SCROLL_SPEED; deltaX *= SCROLL_SPEED;
+ } else if (e.deltaMode === 2) {
+ deltaY *= window.innerHeight; deltaX *= window.innerWidth;
+ } else {
+ const sign = deltaY > 0 ? 1 : -1;
+ deltaY = sign * Math.min(Math.abs(deltaY), SCROLL_SPEED * 3);
+ const signX = deltaX > 0 ? 1 : -1;
+ deltaX = signX * Math.min(Math.abs(deltaX), SCROLL_SPEED * 3);
+ }
+ e.preventDefault();
+ scrollable.scrollBy({ top: deltaY, left: e.shiftKey ? deltaX : 0, behavior: 'auto' });
+ }, { passive: false, capture: true });
+})();
+`;
+
+function getDesktopViewState(win) {
+ if (!win) return null;
+ let state = desktopViewStateByWindowId.get(win.id);
+ if (!state) {
+ state = {
+ views: new Map(), // tabId -> BrowserView
+ activeTabId: null,
+ bounds: null
+ };
+ desktopViewStateByWindowId.set(win.id, state);
+ }
+ return state;
+}
+
+function createMenuPopupWindow(parentWin) {
+ const menuWin = new BrowserWindow({
+ parent: parentWin,
+ modal: false,
+ frame: false,
+ transparent: true,
+ resizable: false,
+ show: false,
+ alwaysOnTop: true,
+ skipTaskbar: true,
+ focusable: true,
+ hasShadow: true,
+ webPreferences: {
+ preload: path.join(__dirname, 'preload.js'),
+ nodeIntegration: false,
+ contextIsolation: true,
+ sandbox: false,
+ partition: 'persist:main'
+ }
+ });
+
+ menuWin.setMenu(null);
+ try { menuWin.setAlwaysOnTop(true, 'pop-up-menu'); } catch {}
+
+ const hideMenu = () => {
+ if (!menuWin.isDestroyed()) menuWin.hide();
+ };
+
+ menuWin.on('blur', hideMenu);
+ parentWin.on('move', hideMenu);
+ parentWin.on('resize', hideMenu);
+
+ menuWin.on('closed', () => {
+ try { menuPopupByWindowId.delete(parentWin.id); } catch {}
+ try { parentWin.removeListener('move', hideMenu); } catch {}
+ try { parentWin.removeListener('resize', hideMenu); } catch {}
+ });
+
+ menuWin.loadFile(path.join(__dirname, 'renderer', 'menu-popup.html'));
+ return menuWin;
+}
+
+function positionMenuPopup(parentWin, menuWin, anchorRect) {
+ if (!parentWin || !menuWin || !anchorRect) return;
+ const contentBounds = parentWin.getContentBounds();
+ const display = screen.getDisplayMatching(contentBounds);
+ const workArea = display?.workArea || contentBounds;
+
+ const width = MENU_POPUP_SIZE.width;
+ const height = MENU_POPUP_SIZE.height;
+ let x = Math.round(contentBounds.x + anchorRect.x + anchorRect.width - width);
+ let y = Math.round(contentBounds.y + anchorRect.y + anchorRect.height + 6);
+
+ if (x < workArea.x) x = workArea.x;
+ if (y < workArea.y) y = workArea.y;
+ if (x + width > workArea.x + workArea.width) x = workArea.x + workArea.width - width;
+ if (y + height > workArea.y + workArea.height) y = workArea.y + workArea.height - height;
+
+ menuWin.setBounds({ x, y, width, height }, false);
+}
+
+function getOwnerWindowForContents(contents) {
+ if (!contents) return null;
+ try {
+ if (contents.hostWebContents) {
+ return BrowserWindow.fromWebContents(contents.hostWebContents);
+ }
+ } catch {}
+ try {
+ const maybeWin = BrowserWindow.fromWebContents(contents);
+ if (maybeWin) return maybeWin;
+ } catch {}
+ const mapped = desktopViewByWebContentsId.get(contents.id);
+ return mapped?.win || null;
+}
+
+function getActiveDesktopViewWebContents(win) {
+ const state = getDesktopViewState(win);
+ if (!state || !state.activeTabId) return null;
+ const view = state.views.get(state.activeTabId);
+ return view?.webContents || null;
+}
+
+function sendBrowserViewEvent(win, payload) {
+ try {
+ if (win && !win.isDestroyed()) {
+ win.webContents.send('browserview-event', payload);
+ }
+ } catch {}
+}
+
+function createBrowserViewForTab(win, tabId, url) {
+ const state = getDesktopViewState(win);
+ if (!state) return null;
+ if (state.views.has(tabId)) return state.views.get(tabId);
+
+ const view = new BrowserView({
+ webPreferences: {
+ preload: path.join(__dirname, 'preload.js'),
+ nodeIntegration: false,
+ contextIsolation: true,
+ partition: 'persist:main',
+ sandbox: false,
+ webSecurity: true,
+ allowRunningInsecureContent: false,
+ nativeWindowOpen: false,
+ additionalArguments: [`--nebula-tab-id=${tabId}`]
+ }
+ });
+
+ try {
+ if (!process.env.NEBULA_DEBUG_ELECTRON_UA) {
+ view.webContents.setUserAgent(app.userAgentFallback || computeBaseUA());
+ }
+ } catch {}
+
+ state.views.set(tabId, view);
+ desktopViewByWebContentsId.set(view.webContents.id, { win, tabId, view });
+
+ view.webContents.on('page-title-updated', (_e, title) => {
+ sendBrowserViewEvent(win, { tabId, type: 'page-title-updated', title });
+ });
+
+ view.webContents.on('destroyed', () => {
+ try { desktopViewByWebContentsId.delete(view.webContents.id); } catch {}
+ try { state.views.delete(tabId); } catch {}
+ if (state.activeTabId === tabId) state.activeTabId = null;
+ });
+
+ view.webContents.on('page-favicon-updated', (_e, favicons) => {
+ sendBrowserViewEvent(win, { tabId, type: 'page-favicon-updated', favicons });
+ });
+
+ view.webContents.on('did-navigate', (_e, url) => {
+ sendBrowserViewEvent(win, { tabId, type: 'did-navigate', url });
+ });
+
+ view.webContents.on('did-navigate-in-page', (_e, url) => {
+ sendBrowserViewEvent(win, { tabId, type: 'did-navigate-in-page', url });
+ });
+
+ view.webContents.on('did-finish-load', () => {
+ sendBrowserViewEvent(win, { tabId, type: 'did-finish-load' });
+ });
+
+ view.webContents.on('did-fail-load', (_e, errorCode, errorDescription, validatedURL, isMainFrame) => {
+ sendBrowserViewEvent(win, {
+ tabId,
+ type: 'did-fail-load',
+ errorCode,
+ errorDescription,
+ validatedURL,
+ isMainFrame
+ });
+ });
+
+ view.webContents.on('dom-ready', () => {
+ try { view.webContents.insertCSS(SCROLL_NORMALIZATION_CSS); } catch {}
+ try { view.webContents.executeJavaScript(SCROLL_NORMALIZATION_JS, true); } catch {}
+ sendBrowserViewEvent(win, { tabId, type: 'dom-ready' });
+ });
+
+ view.webContents.on('focus', () => {
+ sendBrowserViewEvent(win, { tabId, type: 'focus' });
+ });
+
+ // Route window.open() calls to tabs unless OAuth allowlist matched
+ view.webContents.setWindowOpenHandler((details) => {
+ const { url: targetUrl } = details;
+ if (!/^https?:\/\//i.test(targetUrl)) return { action: 'deny' };
+ const oauthDomains = [
+ 'accounts.google.com',
+ 'login.microsoftonline.com',
+ 'appleid.apple.com',
+ 'github.com/login',
+ 'auth0.com',
+ 'okta.com',
+ 'login.live.com',
+ 'facebook.com/dialog',
+ 'api.twitter.com/oauth',
+ 'discord.com/oauth2'
+ ];
+ const isOAuthDomain = oauthDomains.some(domain => targetUrl.toLowerCase().includes(domain.toLowerCase()));
+ if (isOAuthDomain) return { action: 'allow' };
+ try { win.webContents.send('open-url-new-tab', targetUrl); } catch {}
+ return { action: 'deny' };
+ });
+
+ if (url) {
+ try { view.webContents.loadURL(url); } catch {}
+ }
+
+ return view;
+}
+
+function setActiveBrowserView(win, tabId) {
+ const state = getDesktopViewState(win);
+ if (!state) return null;
+ const view = state.views.get(tabId);
+ if (!view) return null;
+
+ state.activeTabId = tabId;
+ try {
+ win.setBrowserView(view);
+ if (state.bounds) {
+ view.setBounds(state.bounds);
+ }
+ view.setAutoResize({ width: true, height: true });
+ view.webContents.focus();
+ } catch {}
+ return view;
+}
+
+function destroyBrowserView(win, tabId) {
+ const state = getDesktopViewState(win);
+ if (!state) return false;
+ const view = state.views.get(tabId);
+ if (!view) return false;
+ try {
+ if (state.activeTabId === tabId) {
+ try { win.setBrowserView(null); } catch {}
+ state.activeTabId = null;
+ }
+ state.views.delete(tabId);
+ desktopViewByWebContentsId.delete(view.webContents.id);
+ try { view.webContents.destroy(); } catch {}
+ } catch {}
+ return true;
+}
+
+function getZoomTargetForEvent(event) {
+ const win = BrowserWindow.fromWebContents(event.sender);
+ if (!win) return null;
+ if (win.__nebulaMode === 'desktop') {
+ return getActiveDesktopViewWebContents(win) || win.webContents;
+ }
+ return win.webContents;
+}
+
// Initialize portable data paths BEFORE app.ready (must be done early)
// This enables portable mode on all platforms (Windows, macOS, Linux)
// Data is stored in 'user-data' folder within the application directory
@@ -369,7 +671,10 @@ function getScreenInfo() {
*/
function launchBigPictureMode() {
const windows = BrowserWindow.getAllWindows();
- const mainWindow = windows[0];
+ // Prefer the top-level app window (menu popup is a child window)
+ const mainWindow = windows.find(w => {
+ try { return w && !w.isDestroyed() && !w.getParentWindow(); } catch { return false; }
+ });
if (!mainWindow || mainWindow.isDestroyed()) {
console.warn('[BigPicture] No main window available');
@@ -383,6 +688,10 @@ function launchBigPictureMode() {
}
isInBigPictureMode = true;
+
+ // Switch mode and ensure any active BrowserView is detached so it can't cover the UI.
+ try { mainWindow.__nebulaMode = 'bigpicture'; } catch {}
+ try { mainWindow.setBrowserView(null); } catch {}
// Enter fullscreen for Big Picture experience
mainWindow.setFullScreen(true);
@@ -400,7 +709,10 @@ function launchBigPictureMode() {
*/
function exitBigPictureMode() {
const windows = BrowserWindow.getAllWindows();
- const mainWindow = windows[0];
+ // Prefer the top-level app window (menu popup is a child window)
+ const mainWindow = windows.find(w => {
+ try { return w && !w.isDestroyed() && !w.getParentWindow(); } catch { return false; }
+ });
if (!mainWindow || mainWindow.isDestroyed()) {
console.warn('[BigPicture] No main window to exit from');
@@ -413,6 +725,9 @@ function exitBigPictureMode() {
}
isInBigPictureMode = false;
+
+ // Restore desktop mode and (after the UI reloads) reattach the active BrowserView.
+ try { mainWindow.__nebulaMode = 'desktop'; } catch {}
// Exit fullscreen and restore normal window
mainWindow.setFullScreen(false);
@@ -420,6 +735,21 @@ function exitBigPictureMode() {
// Navigate back to desktop UI
mainWindow.loadFile('renderer/index.html');
+
+ try {
+ mainWindow.webContents.once('did-finish-load', () => {
+ try {
+ const state = getDesktopViewState(mainWindow);
+ const tabId = state?.activeTabId;
+ const view = tabId ? state?.views?.get(tabId) : null;
+ if (view) {
+ try { mainWindow.setBrowserView(view); } catch {}
+ try { if (state.bounds) view.setBounds(state.bounds); } catch {}
+ try { view.setAutoResize({ width: true, height: true }); } catch {}
+ }
+ } catch {}
+ });
+ } catch {}
// Maximize on Windows after exiting fullscreen
if (process.platform === 'win32') {
@@ -551,6 +881,17 @@ function createWindow(startUrl, bigPictureMode = false) {
}
const win = new BrowserWindow(windowOptions);
+ win.__nebulaMode = bigPictureMode ? 'bigpicture' : 'desktop';
+ win.on('closed', () => {
+ const state = desktopViewStateByWindowId.get(win.id);
+ if (state) {
+ for (const view of state.views.values()) {
+ try { desktopViewByWebContentsId.delete(view.webContents.id); } catch {}
+ try { view.webContents.destroy(); } catch {}
+ }
+ desktopViewStateByWindowId.delete(win.id);
+ }
+ });
perfMarks.browserWindow_instantiated = performance.now();
// Intercept window.open() requests and route them into the existing window as a new tab
@@ -824,8 +1165,14 @@ app.whenReady().then(() => {
// --- Auto-Updater Setup ---
// Configure auto-updater logging
- autoUpdater.logger = require('electron-updater').autoUpdater.logger;
- if (autoUpdater.logger) autoUpdater.logger.transports.file.level = 'info';
+ try {
+ autoUpdater.logger = require('electron-updater').autoUpdater.logger;
+ if (autoUpdater.logger && autoUpdater.logger.transports && autoUpdater.logger.transports.file) {
+ autoUpdater.logger.transports.file.level = 'info';
+ }
+ } catch (err) {
+ console.log('[AutoUpdater] Could not configure logger:', err.message);
+ }
// Check for updates after a short delay to not block startup
setTimeout(() => {
@@ -1155,12 +1502,13 @@ ipcMain.handle('clear-search-history', async () => {
});
ipcMain.handle('get-zoom-factor', event => {
- const wc = BrowserWindow.fromWebContents(event.sender).webContents;
- return wc.getZoomFactor();
+ const wc = getZoomTargetForEvent(event);
+ return wc ? wc.getZoomFactor() : 1.0;
});
ipcMain.handle('zoom-in', event => {
- const wc = BrowserWindow.fromWebContents(event.sender).webContents;
+ const wc = getZoomTargetForEvent(event);
+ if (!wc) return 1.0;
const current = wc.getZoomFactor();
const z = Math.min(current + 0.1, 3);
wc.setZoomFactor(z);
@@ -1169,7 +1517,8 @@ ipcMain.handle('zoom-in', event => {
ipcMain.handle('zoom-out', event => {
- const wc = BrowserWindow.fromWebContents(event.sender).webContents;
+ const wc = getZoomTargetForEvent(event);
+ if (!wc) return 1.0;
const current = wc.getZoomFactor();
const z = Math.max(current - 0.1, 0.25);
wc.setZoomFactor(z);
@@ -1192,7 +1541,7 @@ ipcMain.handle('get-display-scale', async (event) => {
});
ipcMain.handle('set-zoom-factor', (event, zoomFactor) => {
- const wc = BrowserWindow.fromWebContents(event.sender).webContents;
+ const wc = getZoomTargetForEvent(event);
if (wc && typeof wc.setZoomFactor === 'function') {
wc.setZoomFactor(zoomFactor);
return true;
@@ -1298,9 +1647,11 @@ ipcMain.handle('get-about-info', () => {
// Toggle DevTools for the requesting window (main window webContents)
ipcMain.handle('open-devtools', (event) => {
- const wc = BrowserWindow.fromWebContents(event.sender);
- if (!wc) return false;
- const contents = wc.webContents;
+ const win = BrowserWindow.fromWebContents(event.sender);
+ if (!win) return false;
+ const contents = win.__nebulaMode === 'desktop'
+ ? (getActiveDesktopViewWebContents(win) || win.webContents)
+ : win.webContents;
if (contents.isDevToolsOpened()) {
contents.closeDevTools();
} else {
@@ -1310,6 +1661,231 @@ ipcMain.handle('open-devtools', (event) => {
return contents.isDevToolsOpened();
});
+// =============================================================================
+// BrowserView IPC (desktop mode tabs)
+// =============================================================================
+ipcMain.handle('browserview-create', (event, { tabId, url }) => {
+ try {
+ const win = BrowserWindow.fromWebContents(event.sender);
+ if (!win) return { success: false, error: 'no-window' };
+ if (win.__nebulaMode !== 'desktop') return { success: false, error: 'not-desktop' };
+ const view = createBrowserViewForTab(win, tabId, url);
+ return { success: !!view };
+ } catch (err) {
+ return { success: false, error: err.message };
+ }
+});
+
+ipcMain.handle('browserview-set-active', (event, { tabId }) => {
+ try {
+ const win = BrowserWindow.fromWebContents(event.sender);
+ if (!win) return false;
+ if (win.__nebulaMode !== 'desktop') return false;
+ return !!setActiveBrowserView(win, tabId);
+ } catch {
+ return false;
+ }
+});
+
+ipcMain.handle('browserview-destroy', (event, { tabId }) => {
+ try {
+ const win = BrowserWindow.fromWebContents(event.sender);
+ if (!win) return false;
+ if (win.__nebulaMode !== 'desktop') return false;
+ return destroyBrowserView(win, tabId);
+ } catch {
+ return false;
+ }
+});
+
+ipcMain.handle('browserview-load-url', (event, { tabId, url }) => {
+ try {
+ const win = BrowserWindow.fromWebContents(event.sender);
+ if (!win) return false;
+ if (win.__nebulaMode !== 'desktop') return false;
+ const state = getDesktopViewState(win);
+ const view = state?.views.get(tabId);
+ if (!view) return false;
+ view.webContents.loadURL(url);
+ return true;
+ } catch {
+ return false;
+ }
+});
+
+ipcMain.handle('browserview-reload', (event, { tabId, ignoreCache }) => {
+ try {
+ const win = BrowserWindow.fromWebContents(event.sender);
+ if (!win) return false;
+ if (win.__nebulaMode !== 'desktop') return false;
+ const state = getDesktopViewState(win);
+ const view = state?.views.get(tabId);
+ if (!view) return false;
+ if (ignoreCache && typeof view.webContents.reloadIgnoringCache === 'function') {
+ view.webContents.reloadIgnoringCache();
+ } else {
+ view.webContents.reload();
+ }
+ return true;
+ } catch {
+ return false;
+ }
+});
+
+ipcMain.handle('browserview-get-url', (event, { tabId }) => {
+ try {
+ const win = BrowserWindow.fromWebContents(event.sender);
+ if (!win) return null;
+ if (win.__nebulaMode !== 'desktop') return null;
+ const state = getDesktopViewState(win);
+ const view = state?.views.get(tabId);
+ return view?.webContents.getURL() || null;
+ } catch {
+ return null;
+ }
+});
+
+ipcMain.handle('browserview-execute-js', async (event, { tabId, code }) => {
+ try {
+ const win = BrowserWindow.fromWebContents(event.sender);
+ if (!win) return null;
+ if (win.__nebulaMode !== 'desktop') return null;
+ const state = getDesktopViewState(win);
+ const view = state?.views.get(tabId);
+ if (!view) return null;
+ return await view.webContents.executeJavaScript(code, true);
+ } catch {
+ return null;
+ }
+});
+
+ipcMain.handle('browserview-set-bounds', (event, bounds) => {
+ try {
+ const win = BrowserWindow.fromWebContents(event.sender);
+ if (!win) return false;
+ if (win.__nebulaMode !== 'desktop') return false;
+ const state = getDesktopViewState(win);
+ if (!state) return false;
+ const safeBounds = {
+ x: Math.max(0, Math.round(bounds?.x || 0)),
+ y: Math.max(0, Math.round(bounds?.y || 0)),
+ width: Math.max(0, Math.round(bounds?.width || 0)),
+ height: Math.max(0, Math.round(bounds?.height || 0))
+ };
+ state.bounds = safeBounds;
+ if (state.activeTabId) {
+ const view = state.views.get(state.activeTabId);
+ if (view) {
+ view.setBounds(safeBounds);
+ }
+ }
+ return true;
+ } catch {
+ return false;
+ }
+});
+
+// Overlay menu (to sit above BrowserView)
+ipcMain.on('menu-popup-toggle', (event, payload = {}) => {
+ try {
+ const parentWin = BrowserWindow.fromWebContents(event.sender);
+ if (!parentWin) return;
+
+ let menuWin = menuPopupByWindowId.get(parentWin.id);
+ if (!menuWin || menuWin.isDestroyed()) {
+ menuWin = createMenuPopupWindow(parentWin);
+ menuPopupByWindowId.set(parentWin.id, menuWin);
+ }
+
+ if (menuWin.isVisible()) {
+ menuWin.hide();
+ return;
+ }
+
+ positionMenuPopup(parentWin, menuWin, payload.anchorRect);
+
+ const initPayload = { theme: payload.theme || null };
+ const sendInit = () => {
+ try { menuWin.webContents.send('menu-popup-init', initPayload); } catch {}
+ };
+ try {
+ if (menuWin.webContents.isLoadingMainFrame()) {
+ menuWin.webContents.once('did-finish-load', sendInit);
+ } else {
+ sendInit();
+ }
+ } catch {}
+
+ menuWin.show();
+ menuWin.focus();
+ } catch {}
+});
+
+ipcMain.on('menu-popup-hide', (event) => {
+ try {
+ const parentWin = BrowserWindow.fromWebContents(event.sender);
+ if (!parentWin) return;
+ const menuWin = menuPopupByWindowId.get(parentWin.id);
+ if (menuWin && !menuWin.isDestroyed()) menuWin.hide();
+ } catch {}
+});
+
+ipcMain.on('menu-popup-command', (event, payload = {}) => {
+ try {
+ const menuWin = BrowserWindow.fromWebContents(event.sender);
+ const parentWin = menuWin?.getParentWindow();
+ if (menuWin && !menuWin.isDestroyed()) menuWin.hide();
+ if (!parentWin || parentWin.isDestroyed()) return;
+ if (!payload?.cmd || payload.cmd === 'close') return;
+ parentWin.webContents.send('menu-command', payload);
+ } catch {}
+});
+
+ipcMain.on('browserview-broadcast', (event, { channel, args }) => {
+ try {
+ const win = BrowserWindow.fromWebContents(event.sender);
+ if (!win) return;
+ if (win.__nebulaMode !== 'desktop') return;
+ const state = getDesktopViewState(win);
+ if (!state) return;
+ for (const view of state.views.values()) {
+ try { view.webContents.send(channel, ...(args || [])); } catch {}
+ }
+ } catch {}
+});
+
+ipcMain.on('browserview-host-message', (event, payload = {}) => {
+ try {
+ const { tabId, channel, args } = payload || {};
+ console.log('[IPC Main] browserview-host-message received, tabId:', tabId, 'channel:', channel);
+
+ let win = getOwnerWindowForContents(event.sender);
+ console.log('[IPC Main] getOwnerWindowForContents returned:', win ? 'window found' : 'null');
+
+ if (!win && tabId) {
+ console.log('[IPC Main] Trying to find window by tabId...');
+ for (const candidate of BrowserWindow.getAllWindows()) {
+ const state = desktopViewStateByWindowId.get(candidate.id);
+ console.log('[IPC Main] Checking window', candidate.id, 'state:', state ? 'found' : 'null', 'has tabId:', state?.views?.has(tabId));
+ if (state && state.views && state.views.has(tabId)) {
+ win = candidate;
+ console.log('[IPC Main] Found window by tabId');
+ break;
+ }
+ }
+ }
+
+ if (!win || win.isDestroyed()) {
+ console.log('[IPC Main] No valid window found, returning');
+ return;
+ }
+ console.log('[IPC Main] Forwarding to renderer');
+ win.webContents.send('browserview-host-message', { tabId, channel, args: args || [] });
+ } catch (err) {
+ console.error('[IPC Main] Error:', err);
+ }
+});
+
// Helper function to read package.json version
function getInstalledElectronVersion() {
try {
@@ -1524,7 +2100,8 @@ ipcMain.handle('show-open-file-dialog', async () => {
// 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 ownerWin = getOwnerWindowForContents(sender);
+ const embedder = ownerWin?.webContents || sender.hostWebContents || sender;
const template = [];
template.push(
@@ -1584,21 +2161,19 @@ function buildAndShowContextMenu(sender, params = {}) {
label: 'Inspect Element',
click: () => {
try {
- // Use the main window's webContents for DevTools
- const mainWin = BrowserWindow.fromWebContents(sender.hostWebContents || sender);
- const mainWC = mainWin.webContents;
+ const inspectTarget = sender;
const inspectX = params.x ?? params.clientX ?? 0;
const inspectY = params.y ?? params.clientY ?? 0;
// Open DevTools docked at bottom if not already open
- if (!mainWC.isDevToolsOpened()) {
- mainWC.openDevTools({ mode: 'bottom' });
+ if (!inspectTarget.isDevToolsOpened()) {
+ inspectTarget.openDevTools({ mode: 'bottom' });
}
// Inspect the element
setTimeout(() => {
try {
- mainWC.inspectElement(inspectX, inspectY);
+ inspectTarget.inspectElement(inspectX, inspectY);
} catch (e) {
// Fallback: try on original sender
try { sender.inspectElement(inspectX, inspectY); } catch {}
@@ -1613,7 +2188,7 @@ function buildAndShowContextMenu(sender, params = {}) {
// Allow plugins to customize/append context menu
try { pluginManager.applyContextMenuContrib(template, params, sender); } catch {}
const menu = Menu.buildFromTemplate(template);
- const win = BrowserWindow.fromWebContents(embedder);
+ const win = ownerWin || BrowserWindow.fromWebContents(embedder);
if (win) menu.popup({ window: win });
} catch (err) {
console.error('Failed to build context menu:', err);
diff --git a/preload.js b/preload.js
index 9d5d17d..7b6ea5e 100644
--- a/preload.js
+++ b/preload.js
@@ -10,6 +10,13 @@ try {
fsModule = null;
}
+// BrowserView tab id (desktop mode) injected via additionalArguments
+let nebulaTabId = null;
+try {
+ const arg = (process?.argv || []).find(a => typeof a === 'string' && a.startsWith('--nebula-tab-id='));
+ if (arg) nebulaTabId = arg.split('=')[1] || null;
+} catch {}
+
// =============================================================================
// GAMEPAD HANDLER - Steam Deck / SteamOS Support
// =============================================================================
@@ -272,10 +279,19 @@ const electronAPI = {
console.error('IPC send error:', err);
}
},
- // Send message to embedding page (webview host)
+ // Send message to embedding page (webview host) or to BrowserView host
sendToHost: (ch, ...args) => {
try {
- return ipcRenderer.sendToHost(ch, ...args);
+ // If running in BrowserView context, ALWAYS use browserview-host-message
+ if (nebulaTabId) {
+ return ipcRenderer.send('browserview-host-message', { tabId: nebulaTabId, channel: ch, args });
+ }
+ // Otherwise try ipcRenderer.sendToHost (for webview contexts)
+ if (typeof ipcRenderer.sendToHost === 'function') {
+ return ipcRenderer.sendToHost(ch, ...args);
+ }
+ // Final fallback
+ return ipcRenderer.send(ch, ...args);
} catch (err) {
console.error('IPC sendToHost error:', err);
}
diff --git a/renderer/bigpicture.css b/renderer/bigpicture.css
index f24f426..1c185fb 100644
--- a/renderer/bigpicture.css
+++ b/renderer/bigpicture.css
@@ -240,7 +240,8 @@ body.mouse-active {
}
.bp-exit-btn:hover,
-.bp-exit-btn:focus {
+.bp-exit-btn:focus,
+.bp-exit-btn.focused {
background: var(--bp-surface-hover);
border-color: var(--bp-primary);
box-shadow: var(--bp-focus-ring);
@@ -306,7 +307,8 @@ body.mouse-active {
color: var(--bp-text);
}
-.nav-item:focus {
+.nav-item:focus,
+.nav-item.focused {
outline: none;
background: var(--bp-surface-hover);
border-color: var(--bp-primary);
@@ -314,7 +316,8 @@ body.mouse-active {
color: var(--bp-text);
}
-.nav-item:focus .material-symbols-outlined {
+.nav-item:focus .material-symbols-outlined,
+.nav-item.focused .material-symbols-outlined {
transform: scale(1.1);
}
@@ -449,7 +452,8 @@ body.mouse-active {
border-color: var(--bp-text-dim);
}
-.action-btn:focus {
+.action-btn:focus,
+.action-btn.focused {
outline: none;
border-color: var(--bp-primary);
box-shadow: var(--bp-focus-ring);
@@ -461,7 +465,8 @@ body.mouse-active {
}
.action-btn.danger:hover,
-.action-btn.danger:focus {
+.action-btn.danger:focus,
+.action-btn.danger.focused {
border-color: var(--bp-danger);
color: var(--bp-danger);
}
@@ -495,7 +500,8 @@ body.mouse-active {
}
.search-card:focus,
-.search-card:focus-within {
+.search-card:focus-within,
+.search-card.focused {
outline: none;
border-color: var(--bp-accent);
box-shadow: var(--bp-focus-ring-accent);
@@ -599,14 +605,16 @@ body.mouse-active {
opacity: 1;
}
-.tile:focus {
+.tile:focus,
+.tile.focused {
outline: none;
border-color: var(--bp-primary);
box-shadow: var(--bp-focus-ring);
transform: scale(1.02);
}
-.tile:focus::before {
+.tile:focus::before,
+.tile.focused::before {
opacity: 1;
}
@@ -671,13 +679,15 @@ body.mouse-active {
}
.tile.add-tile:hover,
-.tile.add-tile:focus {
+.tile.add-tile:focus,
+.tile.add-tile.focused {
border-color: var(--bp-accent);
border-style: solid;
}
.tile.add-tile:hover .material-symbols-outlined,
-.tile.add-tile:focus .material-symbols-outlined {
+.tile.add-tile:focus .material-symbols-outlined,
+.tile.add-tile.focused .material-symbols-outlined {
color: var(--bp-accent);
}
@@ -722,7 +732,8 @@ body.mouse-active {
transform: translateY(-4px);
}
-.scroll-card:focus {
+.scroll-card:focus,
+.scroll-card.focused {
outline: none;
border-color: var(--bp-primary);
box-shadow: var(--bp-focus-ring);
@@ -795,7 +806,8 @@ body.mouse-active {
background: var(--bp-surface-hover);
}
-.list-item:focus {
+.list-item:focus,
+.list-item.focused {
outline: none;
border-color: var(--bp-primary);
box-shadow: var(--bp-focus-ring);
@@ -910,7 +922,8 @@ body.mouse-active {
transform: scale(1.02);
}
-.nebot-card:focus {
+.nebot-card:focus,
+.nebot-card.focused {
outline: none;
border-color: var(--bp-accent);
box-shadow: var(--bp-focus-ring-accent);
@@ -974,7 +987,8 @@ body.mouse-active {
transform: scale(1.02);
}
-.settings-card:focus {
+.settings-card:focus,
+.settings-card.focused {
outline: none;
border-color: var(--bp-primary);
box-shadow: var(--bp-focus-ring);
@@ -1028,7 +1042,8 @@ body.mouse-active {
color: var(--bp-text);
}
-.settings-tab:focus {
+.settings-tab:focus,
+.settings-tab.focused {
outline: none;
border-color: var(--bp-primary);
box-shadow: var(--bp-focus-ring);
@@ -1091,14 +1106,9 @@ body.mouse-active {
transform: translateY(-2px);
}
-.theme-card:focus {
- outline: none;
- border-color: var(--bp-primary);
- box-shadow: var(--bp-focus-ring);
- transform: translateY(-2px) scale(1.02);
-}
-
+.theme-card:focus,
.theme-card.focused {
+ outline: none;
border-color: var(--bp-primary);
box-shadow: var(--bp-focus-ring);
transform: translateY(-2px) scale(1.02);
@@ -1187,18 +1197,13 @@ body.mouse-active {
border-color: var(--bp-primary);
}
-.scale-btn:focus {
+.scale-btn:focus,
+.scale-btn.focused {
outline: none;
border-color: var(--bp-primary);
box-shadow: var(--bp-focus-ring);
}
-.scale-btn.focused {
- border-color: var(--bp-primary);
- box-shadow: var(--bp-focus-ring);
- background: var(--bp-primary);
-}
-
.scale-value {
min-width: 60px;
text-align: center;
@@ -1227,18 +1232,13 @@ body.mouse-active {
border-color: var(--bp-primary);
}
-.action-button:focus {
+.action-button:focus,
+.action-button.focused {
outline: none;
border-color: var(--bp-primary);
box-shadow: var(--bp-focus-ring);
}
-.action-button.focused {
- border-color: var(--bp-primary);
- box-shadow: var(--bp-focus-ring);
- background: var(--bp-primary);
-}
-
.action-button.danger:hover {
background: #dc3545;
border-color: #dc3545;
@@ -1500,7 +1500,8 @@ body.mouse-active {
}
.osk-close:hover,
-.osk-close:focus {
+.osk-close:focus,
+.osk-close.focused {
background: var(--bp-danger);
border-color: var(--bp-danger);
outline: none;
@@ -1542,7 +1543,8 @@ body.mouse-active {
transform: scale(1.05);
}
-.osk-key:focus {
+.osk-key:focus,
+.osk-key.focused {
outline: none;
border-color: var(--bp-accent);
box-shadow: 0 0 0 4px var(--bp-accent-glow), 0 0 20px var(--bp-accent-glow);
@@ -1592,7 +1594,8 @@ body.mouse-active {
}
.osk-action-btn:hover,
-.osk-action-btn:focus {
+.osk-action-btn:focus,
+.osk-action-btn.focused {
background: var(--bp-surface-active);
outline: none;
border-color: var(--bp-accent);
@@ -1604,7 +1607,8 @@ body.mouse-active {
}
.osk-action-btn.primary:hover,
-.osk-action-btn.primary:focus {
+.osk-action-btn.primary:focus,
+.osk-action-btn.primary.focused {
box-shadow: var(--bp-focus-ring);
}
@@ -1676,7 +1680,8 @@ body.mouse-active {
background: var(--bp-surface-hover);
}
-.context-item:focus {
+.context-item:focus,
+.context-item.focused {
outline: none;
border-color: var(--bp-primary);
background: var(--bp-surface-hover);
@@ -1688,7 +1693,8 @@ body.mouse-active {
}
/* Focus indicators for controller navigation */
-[data-focusable]:focus {
+[data-focusable]:focus,
+[data-focusable].focused {
outline: none;
}
diff --git a/renderer/bigpicture.js b/renderer/bigpicture.js
index 38830b6..ab66540 100644
--- a/renderer/bigpicture.js
+++ b/renderer/bigpicture.js
@@ -1912,10 +1912,15 @@ function navigateTo(url) {
webview.style.width = '100%';
webview.style.height = '100%';
webview.style.border = 'none';
- webview.preload = '../preload.js';
+ const preloadPath = window.electronAPI?.getWebviewPreloadPath?.();
+ if (preloadPath) {
+ webview.setAttribute('preload', preloadPath);
+ } else {
+ webview.setAttribute('preload', '../preload.js');
+ }
webview.partition = 'persist:main';
webview.allowpopups = true;
- webview.webpreferences = 'allowRunningInsecureContent=false,javascript=true,webSecurity=true';
+ webview.setAttribute('webpreferences', 'allowRunningInsecureContent=false,javascript=true,webSecurity=true');
container.appendChild(webview);
state.currentWebview = webview;
diff --git a/renderer/index.html b/renderer/index.html
index 0280dd1..1a3470c 100644
--- a/renderer/index.html
+++ b/renderer/index.html
@@ -22,6 +22,11 @@
color: rgba(255, 255, 255, 0.5);
/* Adjust the color and transparency as needed */
}
+
+ #view-host {
+ flex: 1;
+ width: 100%;
+ }
@@ -100,20 +105,7 @@
-
-
-
-
-
-
-
+
diff --git a/renderer/menu-popup.css b/renderer/menu-popup.css
new file mode 100644
index 0000000..ffc9cef
--- /dev/null
+++ b/renderer/menu-popup.css
@@ -0,0 +1,66 @@
+:root {
+ --bg: #0b0d10;
+ --primary: #7b2eff;
+ --accent: #00c6ff;
+ --text: #e0e0e0;
+ --url-bar-bg: #1c2030;
+ --url-bar-border: #3e4652;
+ --shadow-1: 0 12px 30px rgba(0, 0, 0, 0.35);
+ --blur: 12px;
+}
+
+* { box-sizing: border-box; }
+
+body {
+ margin: 0;
+ background: transparent;
+ color: var(--text);
+ font-family: system-ui, -apple-system, Segoe UI, Roboto, sans-serif;
+}
+
+#menu-popup {
+ background: color-mix(in srgb, var(--url-bar-bg) 92%, var(--text) 8%);
+ border: 1px solid color-mix(in srgb, var(--primary) 25%, color-mix(in srgb, var(--accent) 18%, transparent));
+ border-radius: 14px;
+ padding: 8px;
+ display: flex;
+ flex-direction: column;
+ min-width: 220px;
+ box-shadow: var(--shadow-1);
+ -webkit-backdrop-filter: blur(var(--blur));
+ backdrop-filter: blur(var(--blur));
+}
+
+#menu-popup button {
+ background: transparent;
+ border: none;
+ color: var(--text);
+ text-align: left;
+ padding: 8px 10px;
+ border-radius: 10px;
+ cursor: pointer;
+ transition: background 120ms ease, filter 120ms ease;
+}
+
+#menu-popup button:hover {
+ background: color-mix(in srgb, var(--text) 8%, transparent);
+}
+
+.zoom-controls {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 8px;
+ padding: 6px 8px;
+}
+
+.zoom-controls button {
+ width: 28px;
+ height: 28px;
+ text-align: center;
+}
+
+#zoom-percent {
+ min-width: 54px;
+ text-align: center;
+}
diff --git a/renderer/menu-popup.html b/renderer/menu-popup.html
new file mode 100644
index 0000000..3e8678c
--- /dev/null
+++ b/renderer/menu-popup.html
@@ -0,0 +1,23 @@
+
+
+
+
+ Menu
+
+
+
+
+
+
+
diff --git a/renderer/menu-popup.js b/renderer/menu-popup.js
new file mode 100644
index 0000000..901598f
--- /dev/null
+++ b/renderer/menu-popup.js
@@ -0,0 +1,46 @@
+const zoomPercentEl = document.getElementById('zoom-percent');
+
+function setCssVar(name, value, fallback) {
+ const val = value || fallback;
+ if (val) document.documentElement.style.setProperty(name, val);
+}
+
+function applyTheme(theme) {
+ const colors = theme?.colors || theme || {};
+ setCssVar('--bg', colors.bg, '#0b0d10');
+ setCssVar('--dark-blue', colors.darkBlue, '#0b1c2b');
+ setCssVar('--dark-purple', colors.darkPurple, '#1b1035');
+ setCssVar('--primary', colors.primary, '#7b2eff');
+ setCssVar('--accent', colors.accent, '#00c6ff');
+ setCssVar('--text', colors.text, '#e0e0e0');
+ setCssVar('--url-bar-bg', colors.urlBarBg, '#1c2030');
+ setCssVar('--url-bar-border', colors.urlBarBorder, '#3e4652');
+}
+
+async function refreshZoom() {
+ if (!window.electronAPI?.invoke || !zoomPercentEl) return;
+ try {
+ const z = await window.electronAPI.invoke('get-zoom-factor');
+ zoomPercentEl.textContent = `${Math.round(z * 100)}%`;
+ } catch {}
+}
+
+window.electronAPI?.on?.('menu-popup-init', (payload) => {
+ applyTheme(payload?.theme);
+ refreshZoom();
+});
+
+window.addEventListener('click', (e) => {
+ const btn = e.target.closest('button[data-cmd]');
+ if (!btn) return;
+ const cmd = btn.getAttribute('data-cmd');
+ window.electronAPI?.send?.('menu-popup-command', { cmd });
+});
+
+window.addEventListener('keydown', (e) => {
+ if (e.key === 'Escape') {
+ window.electronAPI?.send?.('menu-popup-command', { cmd: 'close' });
+ }
+});
+
+refreshZoom();
diff --git a/renderer/script.js b/renderer/script.js
index 08759cf..519ec26 100644
--- a/renderer/script.js
+++ b/renderer/script.js
@@ -268,10 +268,25 @@ function applyThemeToMainUI(theme) {
// 1) cache hot DOM references
const urlBox = document.getElementById('url');
const tabBarEl = document.getElementById('tab-bar');
-const webviewsEl = document.getElementById('webviews');
+const viewHostEl = document.getElementById('view-host');
const menuPopup = document.getElementById('menu-popup');
// (Removed old custom HTML context menu in favor of native Electron menu)
+function updateBrowserViewBounds() {
+ if (!viewHostEl) return;
+ const rect = viewHostEl.getBoundingClientRect();
+ ipcRenderer.invoke('browserview-set-bounds', {
+ x: rect.left,
+ y: rect.top,
+ width: rect.width,
+ height: rect.height
+ }).catch(() => {});
+}
+
+window.addEventListener('resize', () => {
+ updateBrowserViewBounds();
+});
+
// Select all text on focus and prevent mouseup from deselecting
urlBox.addEventListener('focus', () => {
urlBox.select();
@@ -430,6 +445,71 @@ ipcRenderer.on('open-url-new-tab', (url) => {
if (typeof url === 'string' && url) createTab(url);
});
+// Messages from BrowserView pages (sendToHost fallback)
+ipcRenderer.on('browserview-host-message', (payload) => {
+ console.log('[Renderer] browserview-host-message received:', payload);
+ const data = payload || {};
+ const channel = data.channel;
+ const args = data.args || [];
+ if (!channel) return;
+
+ if (channel === 'navigate' && args[0]) {
+ console.log('[Renderer] Navigating to:', args[0]);
+ const targetUrl = args[0];
+ const opts = args[1] || {};
+ try {
+ if (opts.insecureBypass && /^http:\/\//i.test(targetUrl)) {
+ const h = new URL(targetUrl).hostname;
+ insecureBypassedHosts.add(h);
+ }
+ } catch {}
+ if (opts.newTab) {
+ createTab(targetUrl);
+ } else {
+ urlBox.value = targetUrl;
+ navigate();
+ }
+ } else if (channel === 'theme-update' && args[0]) {
+ const theme = args[0];
+ applyThemeToMainUI(theme);
+ ipcRenderer.send('browserview-broadcast', { channel: 'theme-update', args: [theme] });
+ }
+});
+
+// Commands from the overlay menu window
+ipcRenderer.on('menu-command', (payload) => {
+ const cmd = payload?.cmd;
+ if (!cmd) return;
+ switch (cmd) {
+ case 'open-settings':
+ openSettings();
+ break;
+ case 'open-downloads':
+ openDownloads();
+ break;
+ case 'toggle-devtools':
+ window.electronAPI?.toggleDevTools?.();
+ break;
+ case 'big-picture':
+ window.bigPictureAPI?.launch?.();
+ break;
+ case 'zoom-in':
+ zoomIn();
+ break;
+ case 'zoom-out':
+ zoomOut();
+ break;
+ case 'hard-reload':
+ hardReload();
+ break;
+ case 'fresh-reload':
+ freshReload();
+ break;
+ default:
+ break;
+ }
+});
+
// Auto-open on download start is disabled by design now.
function createTab(inputUrl) {
@@ -442,172 +522,27 @@ function createTab(inputUrl) {
pendingInternalNavigations.push(() => createTab(inputUrl));
return id;
}
-
- // Handle home page specially
- if (inputUrl === 'nebula://home') {
- // Show home container and hide webviews
- const homeContainer = document.getElementById('home-container');
- const webviewsEl = document.getElementById('webviews');
- if (homeContainer) homeContainer.classList.add('active');
- if (webviewsEl) webviewsEl.classList.add('hidden');
- const tab = {
- id,
- url: inputUrl,
- title: 'New Tab',
- favicon: '',
- history: [inputUrl],
- historyIndex: 0,
- isHome: true
- };
- tabs.push(tab);
- setActiveTab(id);
- // Render the tab bar so the new home tab appears
- scheduleRenderTabs();
- return id;
- }
-
- // For all other URLs, use webview
let resolvedUrl = resolveInternalUrl(inputUrl);
console.log('[DEBUG] createTab resolvedUrl:', resolvedUrl, 'from inputUrl:', inputUrl);
- // If it's a raw data: URL (image) keep as is; blob: will only resolve within its origin context (may fail)
- // For very long data URLs we could embed them in a minimal viewer page for cleaner rendering.
- if (resolvedUrl.startsWith('data:') && resolvedUrl.length > 4096) {
- // Create a simple object URL page to avoid huge URL in the address bar (cannot easily persist across restarts).
- const html = ``+
- `
`+
- ``;
- const blob = new Blob([html], { type: 'text/html' });
- resolvedUrl = URL.createObjectURL(blob);
- }
- debug('[DEBUG] createTab() resolvedUrl =', resolvedUrl);
-
- const webview = document.createElement('webview');
- // give the webview an id and set its source and attributes so it actually loads and can be managed
- webview.id = `tab-${id}`;
- webview.src = resolvedUrl;
- webview.setAttribute('allowpopups', '');
- webview.setAttribute('partition', 'persist:main');
- // Use absolute preload path provided by the main-window preload to ensure
- // the guest process can resolve the file (important on Linux/SteamOS).
- try {
- const preloadPath = (window.electronAPI && typeof window.electronAPI.getWebviewPreloadPath === 'function')
- ? window.electronAPI.getWebviewPreloadPath()
- : '../preload.js';
- webview.setAttribute('preload', preloadPath);
- } catch (e) {
- webview.setAttribute('preload', '../preload.js');
- }
- // Add attributes needed for Google OAuth and sign-in flows
- webview.setAttribute('webpreferences', 'allowRunningInsecureContent=false,javascript=true,webSecurity=true');
- try {
- const baseUA = navigator.userAgent.includes('Nebula/') ? navigator.userAgent : navigator.userAgent + ' Nebula/1.0.0';
- webview.setAttribute('useragent', baseUA);
- } catch {
- // fallback: let Electron supply default UA
- }
-
- webview.addEventListener('page-favicon-updated', e => {
- if (e.favicons.length > 0) updateTabMetadata(id, 'favicon', e.favicons[0]);
- });
-
- // Send bookmarks to home page when it loads and apply scroll normalization
- webview.addEventListener('dom-ready', () => {
- // Apply scroll normalization to all sites for consistent scrolling
- applyScrollNormalization(webview);
-
- if (inputUrl === 'nebula://home') {
- webview.executeJavaScript(`
- if (window.receiveBookmarks) {
- window.receiveBookmarks(${JSON.stringify(bookmarks)});
- } else {
- // Store bookmarks for when the page script loads
- window._pendingBookmarks = ${JSON.stringify(bookmarks)};
- }
- `);
- }
- });
-
- // Consolidated navigation recording - only use did-navigate to avoid duplicates
- webview.addEventListener('did-navigate', e => {
- handleNavigation(id, e.url);
- if (e.url.startsWith('http')) debug('[DEBUG] Recording navigation to:', e.url);
- if (/\/cdn-cgi\//.test(e.url) || /challenge/i.test(e.url)) {
- console.log('[Nebula] Cloudflare challenge detected at', e.url);
- }
- });
-
- webview.addEventListener('did-navigate-in-page', e => {
- handleNavigation(id, e.url);
- if (e.url.startsWith('http')) debug('[DEBUG] Recording in-page navigation to:', e.url);
- });
-
- // After load, just refresh nav buttons to avoid jank
- webview.addEventListener('did-finish-load', () => {
- scheduleUpdateNavButtons();
- });
- // Catch failed loads for diagnostics (e.g., http -> https transitions failing)
- webview.addEventListener('did-fail-load', (e) => {
- console.warn('[DEBUG] did-fail-load (createTab) id:', id, 'code:', e.errorCode, 'desc:', e.errorDescription, 'validatedURL:', e.validatedURL, 'isMainFrame:', e.isMainFrame);
- });
-
- // catch any target="_blank" or window.open() calls and open them as new tabs
- webview.addEventListener('new-window', e => {
- // Always open external http(s) targets in a new in-app tab instead of spawning
- // a separate Electron BrowserWindow. (User request)
- // If you need to allow real popup windows for specific OAuth flows later,
- // introduce an allowlist check here before preventDefault().
- if (e.url && /^https?:\/\//i.test(e.url)) {
- e.preventDefault();
- createTab(e.url);
- } else {
- // Block other scheme popups for safety (could extend with custom handling)
- e.preventDefault();
- }
- });
-
- // After creating dynamic webview:
- webview.addEventListener('ipc-message', e => {
- if (e.channel === 'navigate' && e.args[0]) {
- const targetUrl = e.args[0];
- const opts = e.args[1] || {};
- // If user accepted insecure warning, record host to bypass for session
- try {
- if (opts.insecureBypass && /^http:\/\//i.test(targetUrl)) {
- const h = new URL(targetUrl).hostname;
- insecureBypassedHosts.add(h);
- }
- } catch {}
- if (opts.newTab) {
- createTab(targetUrl);
- } else {
- urlBox.value = targetUrl;
- navigate();
- }
- } else if (e.channel === 'theme-update') {
- const theme = e.args[0];
- // Apply theme to main UI
- applyThemeToMainUI(theme);
- const home = document.getElementById('home-webview');
- if (home) home.send('theme-update', theme);
- }
- });
-
- // Ensure interacting with the webview closes any open menu popup
- attachCloseMenuOnInteract(webview);
-
- webviewsEl.appendChild(webview);
+ // Keep data: URLs intact; BrowserView cannot consume blob URLs created in the UI process.
tabs.push({
id,
- url: inputUrl, // ← save the original input like "nebula://home"
+ url: inputUrl,
title: 'New Tab',
favicon: null,
history: [inputUrl],
historyIndex: 0
});
- setActiveTab(id);
+ ipcRenderer.invoke('browserview-create', { tabId: id, url: resolvedUrl })
+ .then(() => {
+ setActiveTab(id);
+ updateBrowserViewBounds();
+ })
+ .catch(() => {});
scheduleRenderTabs();
+ return id;
}
// Expose for plugin usage (e.g., Nebot panel "Open Page")
@@ -645,13 +580,21 @@ function resolveInternalUrl(url) {
console.log('[DEBUG] Resolved plugin page', page, '->', resolved);
return resolved + suffix;
}
- // Fallback: built-in renderer copy (e.g., renderer/nebot.html)
+ // Fallback: built-in renderer copy (resolve to absolute file URL)
console.log('[DEBUG] Using fallback for page:', page);
- if (page === 'nebot') return 'nebot.html' + suffix;
- return `${page}.html${suffix}`;
+ const rel = `${page}.html${suffix}`;
+ try {
+ return new URL(rel, window.location.href).toString();
+ } catch {
+ return rel;
+ }
}
console.log('[DEBUG] Page not in allowedInternalPages, returning 404');
- return '404.html';
+ try {
+ return new URL('404.html', window.location.href).toString();
+ } catch {
+ return '404.html';
+ }
}
// Allow direct loading of common schemes without forcing https://
if (/^(https?:|file:|data:|blob:)/i.test(url)) return url;
@@ -662,8 +605,11 @@ function resolveInternalUrl(url) {
function handleLoadFail(tabId) {
return (event) => {
if (!event.validatedURL.includes('nebula://') && event.errorCode !== -3) {
- const webview = document.getElementById(`tab-${tabId}`);
- webview.src = `404.html?url=${encodeURIComponent(tabs.find(t => t.id === tabId).url)}`;
+ const badUrl = tabs.find(t => t.id === tabId)?.url || '';
+ ipcRenderer.invoke('browserview-load-url', {
+ tabId,
+ url: `404.html?url=${encodeURIComponent(badUrl)}`
+ }).catch(() => {});
}
};
}
@@ -700,7 +646,7 @@ function performNavigation(input, originalInputForHistory) {
resolved = resolveInternalUrl(input);
}
- console.log('[DEBUG] performNavigation input:', input, 'resolved:', resolved, 'tab.isHome:', tab.isHome, 'isInternal:', isInternal);
+ console.log('[DEBUG] performNavigation input:', input, 'resolved:', resolved, 'isInternal:', isInternal);
// Intercept plain HTTP (not HTTPS) navigations (excluding localhost / 127.* / internal pages)
try {
@@ -711,40 +657,12 @@ function performNavigation(input, originalInputForHistory) {
if (!isLoopback && !insecureBypassedHosts.has(host)) {
const encoded = encodeURIComponent(resolved);
// Directly load insecure.html (avoid custom scheme so OS doesn't try to resolve an external handler)
- const interstitial = `insecure.html?target=${encoded}`;
- // For a fresh home tab, convert directly to webview showing the interstitial
- if (tab.isHome) {
- convertHomeTabToWebview(tab.id, originalInputForHistory, interstitial);
- return;
- }
- // Navigate existing webview to interstitial instead
- const webviewExisting = document.getElementById(`tab-${activeTabId}`);
- if (webviewExisting) webviewExisting.src = interstitial;
- tab.history = tab.history.slice(0, tab.historyIndex + 1);
- tab.history.push(originalInputForHistory);
- tab.historyIndex++;
- tab.url = originalInputForHistory;
- scheduleRenderTabs();
- scheduleUpdateNavButtons();
- return;
+ resolved = `insecure.html?target=${encoded}`;
}
}
} catch (e) { debug('[DEBUG] HTTP interception error', e); }
- if (tab.isHome && !isInternal) {
- convertHomeTabToWebview(tab.id, originalInputForHistory, resolved);
- return;
- }
-
- // If this is a home tab and we're navigating to an internal page, convert to webview
- if (tab.isHome && isInternal) {
- convertHomeTabToWebview(tab.id, originalInputForHistory, resolved);
- return;
- }
-
- const webview = document.getElementById(`tab-${activeTabId}`);
- if (!webview) {
- console.log('[DEBUG] No webview found for tab', activeTabId, 'creating new tab instead');
+ if (!activeTabId) {
createTab(input);
return;
}
@@ -752,7 +670,7 @@ function performNavigation(input, originalInputForHistory) {
tab.history.push(originalInputForHistory);
tab.historyIndex++;
tab.url = originalInputForHistory;
- webview.src = resolved;
+ ipcRenderer.invoke('browserview-load-url', { tabId: activeTabId, url: resolved }).catch(() => {});
scheduleRenderTabs();
scheduleUpdateNavButtons();
}
@@ -786,113 +704,6 @@ document.addEventListener('keydown', async (e) => {
}
});
-function convertHomeTabToWebview(tabId, inputUrl, resolvedUrl) {
- const tab = tabs.find(t => t.id === tabId);
- if (!tab) return;
-
- // Ensure webviews container is visible
- const webviewsEl = document.getElementById('webviews');
- if (webviewsEl) webviewsEl.classList.remove('hidden');
- // Create a new webview for this tab
- const webview = document.createElement('webview');
- webview.id = `tab-${tabId}`;
- webview.src = resolvedUrl;
- webview.setAttribute('allowpopups', '');
- webview.setAttribute('partition', 'persist:main');
- try {
- const preloadPath = (window.electronAPI && typeof window.electronAPI.getWebviewPreloadPath === 'function')
- ? window.electronAPI.getWebviewPreloadPath()
- : '../preload.js';
- webview.setAttribute('preload', preloadPath);
- } catch (e) {
- webview.setAttribute('preload', '../preload.js');
- }
- // Add attributes needed for Google OAuth and sign-in flows
- webview.setAttribute('webpreferences', 'allowRunningInsecureContent=false,javascript=true,webSecurity=true');
- try {
- const baseUA2 = navigator.userAgent.includes('Nebula/') ? navigator.userAgent : navigator.userAgent + ' Nebula/1.0.0';
- webview.setAttribute('useragent', baseUA2);
- } catch {}
-
- // Add event listeners
- webview.addEventListener('did-fail-load', handleLoadFail(tabId));
- webview.addEventListener('page-title-updated', e => updateTabMetadata(tabId, 'title', e.title));
- webview.addEventListener('page-favicon-updated', e => {
- if (e.favicons.length > 0) updateTabMetadata(tabId, 'favicon', e.favicons[0]);
- });
-
- webview.addEventListener('did-navigate', e => {
- handleNavigation(tabId, e.url);
- if (/\/cdn-cgi\//.test(e.url) || /challenge/i.test(e.url)) {
- console.log('[Nebula] Cloudflare challenge detected at', e.url);
- }
- });
- webview.addEventListener('did-navigate-in-page', e => {
- handleNavigation(tabId, e.url);
- });
- webview.addEventListener('did-finish-load', () => {
- scheduleUpdateNavButtons();
- });
-
- // Apply scroll normalization when webview is ready
- webview.addEventListener('dom-ready', () => {
- applyScrollNormalization(webview);
- });
-
- webview.addEventListener('new-window', e => {
- // Unified behavior: always open http(s) targets in a new tab (no extra window)
- if (e.url && /^https?:\/\//i.test(e.url)) {
- e.preventDefault();
- createTab(e.url);
- } else {
- e.preventDefault();
- }
- });
-
- // After creating dynamic webview:
- webview.addEventListener('ipc-message', e => {
- if (e.channel === 'theme-update') {
- const theme = e.args && e.args[0];
- if (theme) applyThemeToMainUI(theme);
- const home = document.getElementById('home-webview');
- if (home) home.send('theme-update', ...e.args);
- } else if (e.channel === 'navigate' && e.args[0]) {
- const targetUrl = e.args[0];
- const opts = e.args[1] || {};
- try {
- if (opts.insecureBypass && /^http:\/\//i.test(targetUrl)) {
- const h = new URL(targetUrl).hostname;
- insecureBypassedHosts.add(h);
- }
- } catch {}
- urlBox.value = targetUrl;
- navigate();
- }
- });
-
- // Add webview to DOM
- webviewsEl.appendChild(webview);
-
- // Ensure interacting with the webview closes any open menu popup
- attachCloseMenuOnInteract(webview);
-
- // Update tab properties
- tab.isHome = false;
- tab.webview = webview;
- tab.url = inputUrl;
- // Keep existing history (including home) - the new URL will be added by handleNavigation when webview loads
- // Don't modify historyIndex here - handleNavigation will handle it
-
- // Hide home container and show webview
- const homeContainer = document.getElementById('home-container');
- if (homeContainer) homeContainer.classList.remove('active');
- webview.classList.add('active');
-
- scheduleUpdateNavButtons();
- // Activate converted webview tab and update UI
- setActiveTab(tabId);
- scheduleRenderTabs();
-}
function handleNavigation(tabId, newUrl) {
const tab = tabs.find(t => t.id === tabId);
@@ -980,36 +791,16 @@ function handleNavigation(tabId, newUrl) {
function setActiveTab(id) {
- // hide all individual webviews
- tabs.forEach(t => {
- const w = document.getElementById(`tab-${t.id}`);
- if (w) w.classList.remove('active');
- });
- // toggle containers
- const homeContainer = document.getElementById('home-container');
- const webviewsEl = document.getElementById('webviews');
+ activeTabId = id;
+ ipcRenderer.invoke('browserview-set-active', { tabId: id }).catch(() => {});
+ updateBrowserViewBounds();
const tab = tabs.find(t => t.id === id);
if (tab) {
- if (tab.isHome) {
- homeContainer.classList.add('active');
- webviewsEl.classList.add('hidden');
- } else {
- if (homeContainer) homeContainer.classList.remove('active');
- webviewsEl.classList.remove('hidden');
- const activeWebview = document.getElementById(`tab-${id}`);
- if (activeWebview) activeWebview.classList.add('active');
- }
- }
-
- activeTabId = id;
-
- if (tab) {
- // If the tab URL represents the home page, keep the URL bar blank.
urlBox.value = tab.url === 'nebula://home' ? '' : tab.url;
- scheduleRenderTabs();
+ scheduleRenderTabs();
updateNavButtons();
- updateZoomUI(); // ← update zoom display for new active tab
+ updateZoomUI();
}
}
@@ -1025,9 +816,7 @@ function closeTab(id) {
? (tabs[idx - 1]?.id ?? tabs[idx + 1]?.id ?? tabs[0]?.id)
: activeTabId;
btn.addEventListener('animationend', () => {
- // Remove webview
- const w = document.getElementById(`tab-${id}`);
- if (w) w.remove();
+ ipcRenderer.invoke('browserview-destroy', { tabId: id }).catch(() => {});
// Remove from model
tabs = tabs.filter(t => t.id !== id);
// Choose a new active tab if needed
@@ -1039,8 +828,7 @@ function closeTab(id) {
return;
}
// Fallback (no button rendered yet)
- const w = document.getElementById(`tab-${id}`);
- if (w) w.remove();
+ ipcRenderer.invoke('browserview-destroy', { tabId: id }).catch(() => {});
tabs = tabs.filter(t => t.id !== id);
if (id === activeTabId && tabs.length > 0) setActiveTab(tabs[0].id);
scheduleRenderTabs();
@@ -1259,9 +1047,11 @@ function renderTabs() {
// 1) handle URL sent by main for a detached window
ipcRenderer.on('open-url', (url) => {
+ for (const t of tabs) {
+ ipcRenderer.invoke('browserview-destroy', { tabId: t.id }).catch(() => {});
+ }
tabs = [];
activeTabId = null;
- webviewsEl.innerHTML = '';
tabBarEl.innerHTML = '';
if (typeof url === 'string' && url) createTab(url); else createTab();
});
@@ -1274,40 +1064,9 @@ function goBack() {
if (tab.historyIndex > 0) {
tab.historyIndex--;
const targetUrl = tab.history[tab.historyIndex];
-
- // Special handling for nebula://home - convert webview tab back to home tab
- if (targetUrl === 'nebula://home') {
- const homeContainer = document.getElementById('home-container');
- const webviewsEl = document.getElementById('webviews');
- const webview = document.getElementById(`tab-${activeTabId}`);
-
- // Remove the webview if it exists
- if (webview) {
- webview.remove();
- }
-
- // Convert tab back to home tab
- tab.isHome = true;
- tab.url = targetUrl;
- delete tab.webview;
-
- // Show home container
- if (homeContainer) homeContainer.classList.add('active');
- if (webviewsEl) webviewsEl.classList.add('hidden');
-
- urlBox.value = '';
- scheduleRenderTabs();
- scheduleUpdateNavButtons();
- return;
- }
-
- const webview = document.getElementById(`tab-${activeTabId}`);
- if (webview) {
- isHistoryNavigation = true; // Prevent adding to history during programmatic navigation
- // Resolve internal URLs (nebula://) to actual file paths
- const resolvedUrl = resolveInternalUrl(targetUrl);
- webview.loadURL(resolvedUrl);
- }
+ isHistoryNavigation = true;
+ const resolvedUrl = resolveInternalUrl(targetUrl);
+ ipcRenderer.invoke('browserview-load-url', { tabId: activeTabId, url: resolvedUrl }).catch(() => {});
}
}
@@ -1319,48 +1078,9 @@ function goForward() {
if (tab.historyIndex < tab.history.length - 1) {
tab.historyIndex++;
const targetUrl = tab.history[tab.historyIndex];
-
- // Special handling for nebula://home - it doesn't use a webview
- if (targetUrl === 'nebula://home') {
- const homeContainer = document.getElementById('home-container');
- const webviewsEl = document.getElementById('webviews');
- const webview = document.getElementById(`tab-${activeTabId}`);
-
- // Remove the webview if it exists
- if (webview) {
- webview.remove();
- }
-
- // Convert tab back to home tab
- tab.isHome = true;
- tab.url = targetUrl;
- delete tab.webview;
-
- // Show home container
- if (homeContainer) homeContainer.classList.add('active');
- if (webviewsEl) webviewsEl.classList.add('hidden');
-
- urlBox.value = '';
- scheduleRenderTabs();
- scheduleUpdateNavButtons();
- return;
- }
-
- // Check if we're currently on home and need to create a webview
- if (tab.isHome && targetUrl !== 'nebula://home') {
- // We're going forward from home to a webview page
- const resolvedUrl = resolveInternalUrl(targetUrl);
- convertHomeTabToWebview(activeTabId, targetUrl, resolvedUrl);
- return;
- }
-
- const webview = document.getElementById(`tab-${activeTabId}`);
- if (webview) {
- isHistoryNavigation = true; // Prevent adding to history during programmatic navigation
- // Resolve internal URLs (nebula://) to actual file paths
- const resolvedUrl = resolveInternalUrl(targetUrl);
- webview.loadURL(resolvedUrl);
- }
+ isHistoryNavigation = true;
+ const resolvedUrl = resolveInternalUrl(targetUrl);
+ ipcRenderer.invoke('browserview-load-url', { tabId: activeTabId, url: resolvedUrl }).catch(() => {});
}
}
@@ -1376,35 +1096,29 @@ function updateNavButtons() {
}
function reload() {
- const webview = document.getElementById(`tab-${activeTabId}`);
- if (webview) {
- webview.reload();
- scheduleUpdateNavButtons(); // keep back/forward buttons in sync after a reload
- }
+ if (!activeTabId) return;
+ ipcRenderer.invoke('browserview-reload', { tabId: activeTabId, ignoreCache: false }).catch(() => {});
+ scheduleUpdateNavButtons();
}
function hardReload() {
- const webview = document.getElementById(`tab-${activeTabId}`);
- if (webview && typeof webview.reloadIgnoringCache === 'function') {
- webview.reloadIgnoringCache();
- scheduleUpdateNavButtons();
- } else if (webview) {
- // Fallback
- webview.reload();
- }
+ if (!activeTabId) return;
+ ipcRenderer.invoke('browserview-reload', { tabId: activeTabId, ignoreCache: true }).catch(() => {});
+ scheduleUpdateNavButtons();
}
function freshReload() {
- const webview = document.getElementById(`tab-${activeTabId}`);
- if (!webview) return;
- try {
- const u = new URL(webview.getURL());
- u.searchParams.set('_bust', Date.now().toString());
- webview.src = u.toString();
- } catch {
- // If URL parsing fails (e.g., internal pages), fall back to hard reload
- hardReload();
- }
+ if (!activeTabId) return;
+ ipcRenderer.invoke('browserview-get-url', { tabId: activeTabId }).then((currentUrl) => {
+ if (!currentUrl) return hardReload();
+ try {
+ const u = new URL(currentUrl);
+ u.searchParams.set('_bust', Date.now().toString());
+ ipcRenderer.invoke('browserview-load-url', { tabId: activeTabId, url: u.toString() }).catch(() => {});
+ } catch {
+ hardReload();
+ }
+ });
}
// Function to open the Settings page
@@ -1431,47 +1145,63 @@ let ringSvgEl = null;
// Open/close on button click; stop propagation so outside-click handler doesn't immediately close it
menuBtn.addEventListener('click', (e) => {
e.stopPropagation();
- menuPopup.classList.toggle('hidden');
- if (!menuPopup.classList.contains('hidden')) {
- updateZoomUI(); // ← refresh zoom % whenever menu opens
- }
-});
-
-// Prevent clicks inside the popup from bubbling to the document
-if (menuPopup) {
- menuPopup.addEventListener('click', (e) => e.stopPropagation());
-}
-
-// Close when clicking anywhere outside the menu wrapper
-document.addEventListener('click', (e) => {
- if (!menuPopup || menuPopup.classList.contains('hidden')) return;
- if (menuWrapper && !menuWrapper.contains(e.target)) {
- menuPopup.classList.add('hidden');
- }
+ if (!menuBtn) return;
+ const rect = menuBtn.getBoundingClientRect();
+ const theme = currentThemeColors ? { colors: currentThemeColors } : null;
+ ipcRenderer.send('menu-popup-toggle', {
+ anchorRect: { x: rect.left, y: rect.top, width: rect.width, height: rect.height },
+ theme
+ });
});
// Close on Escape key
document.addEventListener('keydown', (e) => {
- if (e.key === 'Escape' && menuPopup && !menuPopup.classList.contains('hidden')) {
- menuPopup.classList.add('hidden');
- }
+ if (e.key === 'Escape') ipcRenderer.send('menu-popup-hide');
if (e.key === 'Escape' && downloadsPopupEl && !downloadsPopupEl.classList.contains('hidden')) {
hideDownloadsPopup();
}
});
-// Also close when interacting with main content areas (covers webview clicks)
-const homeContainerEl = document.getElementById('home-container');
-if (webviewsEl) {
- webviewsEl.addEventListener('pointerdown', () => {
- if (!menuPopup.classList.contains('hidden')) menuPopup.classList.add('hidden');
- });
-}
-if (homeContainerEl) {
- homeContainerEl.addEventListener('pointerdown', () => {
- if (!menuPopup.classList.contains('hidden')) menuPopup.classList.add('hidden');
- });
-}
+// Close menus when BrowserView receives focus
+ipcRenderer.on('browserview-event', (payload) => {
+ if (!payload || !payload.type) return;
+ const { tabId, type } = payload;
+ if (type === 'focus') {
+ ipcRenderer.send('menu-popup-hide');
+ if (downloadsPopupEl && !downloadsPopupEl.classList.contains('hidden')) hideDownloadsPopup();
+ return;
+ }
+ if (type === 'page-title-updated') {
+ updateTabMetadata(tabId, 'title', payload.title);
+ return;
+ }
+ if (type === 'page-favicon-updated') {
+ const fav = payload.favicons?.[0];
+ if (fav) updateTabMetadata(tabId, 'favicon', fav);
+ return;
+ }
+ if (type === 'did-navigate' || type === 'did-navigate-in-page') {
+ if (payload.url) {
+ handleNavigation(tabId, payload.url);
+ if (/\/cdn-cgi\//.test(payload.url) || /challenge/i.test(payload.url)) {
+ console.log('[Nebula] Cloudflare challenge detected at', payload.url);
+ }
+ }
+ return;
+ }
+ if (type === 'did-finish-load') {
+ scheduleUpdateNavButtons();
+ return;
+ }
+ if (type === 'did-fail-load') {
+ handleLoadFail(tabId)({
+ validatedURL: payload.validatedURL || '',
+ errorCode: payload.errorCode,
+ errorDescription: payload.errorDescription,
+ isMainFrame: payload.isMainFrame
+ });
+ }
+});
window.addEventListener('DOMContentLoaded', () => {
// Initialize theme from localStorage
@@ -1480,18 +1210,7 @@ window.addEventListener('DOMContentLoaded', () => {
try {
const theme = JSON.parse(savedTheme);
applyThemeToMainUI(theme);
- // Also send to home-webview once it's ready
- const homeWebview = document.getElementById('home-webview');
- if (homeWebview) {
- const sendThemeToHome = () => {
- try { homeWebview.send('theme-update', theme); } catch {}
- };
- // If already loaded, send immediately; otherwise wait for dom-ready
- if (homeWebview.getWebContentsId) {
- sendThemeToHome();
- }
- homeWebview.addEventListener('dom-ready', sendThemeToHome, { once: true });
- }
+ ipcRenderer.send('browserview-broadcast', { channel: 'theme-update', args: [theme] });
} catch (err) {
console.error('Error applying saved theme:', err);
}
@@ -1517,43 +1236,7 @@ window.addEventListener('DOMContentLoaded', () => {
// Initial boot
createTab();
- // Handle IPC messages from the static home webview (bookmarks navigation)
- const staticHome = document.getElementById('home-webview');
- if (staticHome) {
- // Close menu when interacting with the home webview
- attachCloseMenuOnInteract(staticHome);
- staticHome.addEventListener('ipc-message', (e) => {
- if (e.channel === 'navigate' && e.args[0]) {
- urlBox.value = e.args[0];
- navigate();
- }
- });
- }
- // Listen for IPC messages from other webviews (e.g., settings)
- webviewsEl.addEventListener('ipc-message', (e) => {
- // Navigation messages from home or other pages
- if (e.channel === 'navigate' && e.args[0]) {
- const targetUrl = e.args[0];
- const opts = e.args[1] || {};
- if (opts.newTab) {
- // Open in a new tab, leaving settings/home intact
- createTab(targetUrl);
- } else {
- urlBox.value = targetUrl;
- navigate();
- }
- }
- // Theme update from settings webview
- if (e.channel === 'theme-update' && e.args[0]) {
- const theme = e.args[0];
- // Apply theme colors to the main renderer
- applyThemeToMainUI(theme);
- const homeWebview = document.getElementById('home-webview');
- if (homeWebview) {
- homeWebview.send('theme-update', theme);
- }
- }
- });
+ updateBrowserViewBounds();
// Fallback: listen for postMessage navigations from embedded pages (home/settings)
window.addEventListener('message', (event) => {
if (event.data && event.data.type === 'navigate' && event.data.url) {
@@ -1593,8 +1276,8 @@ window.addEventListener('DOMContentLoaded', () => {
bigPictureBtn.addEventListener('click', async () => {
try {
await window.bigPictureAPI.launch();
- // Close the menu popup
- if (menuPopup) menuPopup.classList.add('hidden');
+ // Close the overlay menu
+ ipcRenderer.send('menu-popup-hide');
} catch (e) {
console.error('Failed to launch Big Picture Mode:', e);
}
@@ -1779,9 +1462,7 @@ try {
function attachCloseMenuOnInteract(el) {
if (!el) return;
const closeIfOpen = () => {
- if (menuPopup && !menuPopup.classList.contains('hidden')) {
- menuPopup.classList.add('hidden');
- }
+ ipcRenderer.send('menu-popup-hide');
if (downloadsPopupEl && !downloadsPopupEl.classList.contains('hidden')) {
hideDownloadsPopup();
}
@@ -1830,9 +1511,9 @@ window.addEventListener('nebula-context-command', (e) => {
}
// For blob: URLs we need to resolve inside the active webview by converting to dataURL
if (url.startsWith('blob:')) {
- const webview = document.getElementById(`tab-${activeTabId}`);
- if (webview) {
- webview.executeJavaScript(`(async()=>{try{const r=await fetch('${url}');const b=await r.blob();return new Promise(res=>{const fr=new FileReader();fr.onload=()=>res(fr.result);fr.readAsDataURL(b);});}catch(e){return null;}})();`).then(dataUrl=>{
+ if (activeTabId) {
+ const code = `(async()=>{try{const r=await fetch('${url}');const b=await r.blob();return new Promise(res=>{const fr=new FileReader();fr.onload=()=>res(fr.result);fr.readAsDataURL(b);});}catch(e){return null;}})();`;
+ ipcRenderer.invoke('browserview-execute-js', { tabId: activeTabId, code }).then(dataUrl => {
if (dataUrl) {
window.electronAPI.saveImageToDisk('image', dataUrl);
}