Add BrowserView tab system and overlay menu for desktop mode

Introduces a BrowserView-based tab management system for desktop mode, replacing webview elements for tab content. Adds IPC handlers and state management for creating, activating, destroying, and communicating with BrowserViews. Implements an overlay menu popup window for tab actions and zoom controls. Updates renderer UI to use a dedicated view host container, and refactors tab creation and navigation logic to use BrowserViews. Improves focus styling in CSS and updates preload and IPC messaging to support BrowserView contexts.
This commit is contained in:
2026-01-19 20:57:24 +13:00
parent 03a99b7d46
commit a0e76e623d
9 changed files with 1010 additions and 600 deletions
+596 -21
View File
@@ -127,7 +127,7 @@ function initializeSteamworks() {
// This is critical for Steam Input to recognize native controller support // This is critical for Steam Input to recognize native controller support
initializeSteamworks(); 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 // Cleanup Steam callback pump on exit
app.once('before-quit', () => { app.once('before-quit', () => {
@@ -157,6 +157,308 @@ const gpuFallback = new GPUFallback();
const gpuConfig = new GPUConfig(); const gpuConfig = new GPUConfig();
const pluginManager = new PluginManager(); 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) // Initialize portable data paths BEFORE app.ready (must be done early)
// This enables portable mode on all platforms (Windows, macOS, Linux) // This enables portable mode on all platforms (Windows, macOS, Linux)
// Data is stored in 'user-data' folder within the application directory // Data is stored in 'user-data' folder within the application directory
@@ -369,7 +671,10 @@ function getScreenInfo() {
*/ */
function launchBigPictureMode() { function launchBigPictureMode() {
const windows = BrowserWindow.getAllWindows(); 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()) { if (!mainWindow || mainWindow.isDestroyed()) {
console.warn('[BigPicture] No main window available'); console.warn('[BigPicture] No main window available');
@@ -384,6 +689,10 @@ function launchBigPictureMode() {
isInBigPictureMode = true; 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 // Enter fullscreen for Big Picture experience
mainWindow.setFullScreen(true); mainWindow.setFullScreen(true);
mainWindow.setTitle('Nebula - Big Picture Mode'); mainWindow.setTitle('Nebula - Big Picture Mode');
@@ -400,7 +709,10 @@ function launchBigPictureMode() {
*/ */
function exitBigPictureMode() { function exitBigPictureMode() {
const windows = BrowserWindow.getAllWindows(); 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()) { if (!mainWindow || mainWindow.isDestroyed()) {
console.warn('[BigPicture] No main window to exit from'); console.warn('[BigPicture] No main window to exit from');
@@ -414,6 +726,9 @@ function exitBigPictureMode() {
isInBigPictureMode = false; 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 // Exit fullscreen and restore normal window
mainWindow.setFullScreen(false); mainWindow.setFullScreen(false);
mainWindow.setTitle('Nebula'); mainWindow.setTitle('Nebula');
@@ -421,6 +736,21 @@ function exitBigPictureMode() {
// Navigate back to desktop UI // Navigate back to desktop UI
mainWindow.loadFile('renderer/index.html'); 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 // Maximize on Windows after exiting fullscreen
if (process.platform === 'win32') { if (process.platform === 'win32') {
setTimeout(() => { setTimeout(() => {
@@ -551,6 +881,17 @@ function createWindow(startUrl, bigPictureMode = false) {
} }
const win = new BrowserWindow(windowOptions); 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(); perfMarks.browserWindow_instantiated = performance.now();
// Intercept window.open() requests and route them into the existing window as a new tab // 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 --- // --- Auto-Updater Setup ---
// Configure auto-updater logging // Configure auto-updater logging
autoUpdater.logger = require('electron-updater').autoUpdater.logger; try {
if (autoUpdater.logger) autoUpdater.logger.transports.file.level = 'info'; 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 // Check for updates after a short delay to not block startup
setTimeout(() => { setTimeout(() => {
@@ -1155,12 +1502,13 @@ ipcMain.handle('clear-search-history', async () => {
}); });
ipcMain.handle('get-zoom-factor', event => { ipcMain.handle('get-zoom-factor', event => {
const wc = BrowserWindow.fromWebContents(event.sender).webContents; const wc = getZoomTargetForEvent(event);
return wc.getZoomFactor(); return wc ? wc.getZoomFactor() : 1.0;
}); });
ipcMain.handle('zoom-in', event => { 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 current = wc.getZoomFactor();
const z = Math.min(current + 0.1, 3); const z = Math.min(current + 0.1, 3);
wc.setZoomFactor(z); wc.setZoomFactor(z);
@@ -1169,7 +1517,8 @@ ipcMain.handle('zoom-in', event => {
ipcMain.handle('zoom-out', 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 current = wc.getZoomFactor();
const z = Math.max(current - 0.1, 0.25); const z = Math.max(current - 0.1, 0.25);
wc.setZoomFactor(z); wc.setZoomFactor(z);
@@ -1192,7 +1541,7 @@ ipcMain.handle('get-display-scale', async (event) => {
}); });
ipcMain.handle('set-zoom-factor', (event, zoomFactor) => { ipcMain.handle('set-zoom-factor', (event, zoomFactor) => {
const wc = BrowserWindow.fromWebContents(event.sender).webContents; const wc = getZoomTargetForEvent(event);
if (wc && typeof wc.setZoomFactor === 'function') { if (wc && typeof wc.setZoomFactor === 'function') {
wc.setZoomFactor(zoomFactor); wc.setZoomFactor(zoomFactor);
return true; return true;
@@ -1298,9 +1647,11 @@ ipcMain.handle('get-about-info', () => {
// Toggle DevTools for the requesting window (main window webContents) // Toggle DevTools for the requesting window (main window webContents)
ipcMain.handle('open-devtools', (event) => { ipcMain.handle('open-devtools', (event) => {
const wc = BrowserWindow.fromWebContents(event.sender); const win = BrowserWindow.fromWebContents(event.sender);
if (!wc) return false; if (!win) return false;
const contents = wc.webContents; const contents = win.__nebulaMode === 'desktop'
? (getActiveDesktopViewWebContents(win) || win.webContents)
: win.webContents;
if (contents.isDevToolsOpened()) { if (contents.isDevToolsOpened()) {
contents.closeDevTools(); contents.closeDevTools();
} else { } else {
@@ -1310,6 +1661,231 @@ ipcMain.handle('open-devtools', (event) => {
return contents.isDevToolsOpened(); 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 // Helper function to read package.json version
function getInstalledElectronVersion() { function getInstalledElectronVersion() {
try { 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 // Helper to build and show a native context menu for a given webContents + params
function buildAndShowContextMenu(sender, params = {}) { function buildAndShowContextMenu(sender, params = {}) {
try { try {
const embedder = sender.hostWebContents || sender; const ownerWin = getOwnerWindowForContents(sender);
const embedder = ownerWin?.webContents || sender.hostWebContents || sender;
const template = []; const template = [];
template.push( template.push(
@@ -1584,21 +2161,19 @@ function buildAndShowContextMenu(sender, params = {}) {
label: 'Inspect Element', label: 'Inspect Element',
click: () => { click: () => {
try { try {
// Use the main window's webContents for DevTools const inspectTarget = sender;
const mainWin = BrowserWindow.fromWebContents(sender.hostWebContents || sender);
const mainWC = mainWin.webContents;
const inspectX = params.x ?? params.clientX ?? 0; const inspectX = params.x ?? params.clientX ?? 0;
const inspectY = params.y ?? params.clientY ?? 0; const inspectY = params.y ?? params.clientY ?? 0;
// Open DevTools docked at bottom if not already open // Open DevTools docked at bottom if not already open
if (!mainWC.isDevToolsOpened()) { if (!inspectTarget.isDevToolsOpened()) {
mainWC.openDevTools({ mode: 'bottom' }); inspectTarget.openDevTools({ mode: 'bottom' });
} }
// Inspect the element // Inspect the element
setTimeout(() => { setTimeout(() => {
try { try {
mainWC.inspectElement(inspectX, inspectY); inspectTarget.inspectElement(inspectX, inspectY);
} catch (e) { } catch (e) {
// Fallback: try on original sender // Fallback: try on original sender
try { sender.inspectElement(inspectX, inspectY); } catch {} try { sender.inspectElement(inspectX, inspectY); } catch {}
@@ -1613,7 +2188,7 @@ function buildAndShowContextMenu(sender, params = {}) {
// Allow plugins to customize/append context menu // Allow plugins to customize/append context menu
try { pluginManager.applyContextMenuContrib(template, params, sender); } catch {} try { pluginManager.applyContextMenuContrib(template, params, sender); } catch {}
const menu = Menu.buildFromTemplate(template); const menu = Menu.buildFromTemplate(template);
const win = BrowserWindow.fromWebContents(embedder); const win = ownerWin || BrowserWindow.fromWebContents(embedder);
if (win) menu.popup({ window: win }); if (win) menu.popup({ window: win });
} catch (err) { } catch (err) {
console.error('Failed to build context menu:', err); console.error('Failed to build context menu:', err);
+18 -2
View File
@@ -10,6 +10,13 @@ try {
fsModule = null; 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 // GAMEPAD HANDLER - Steam Deck / SteamOS Support
// ============================================================================= // =============================================================================
@@ -272,10 +279,19 @@ const electronAPI = {
console.error('IPC send error:', err); 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) => { sendToHost: (ch, ...args) => {
try { 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) { } catch (err) {
console.error('IPC sendToHost error:', err); console.error('IPC sendToHost error:', err);
} }
+48 -42
View File
@@ -240,7 +240,8 @@ body.mouse-active {
} }
.bp-exit-btn:hover, .bp-exit-btn:hover,
.bp-exit-btn:focus { .bp-exit-btn:focus,
.bp-exit-btn.focused {
background: var(--bp-surface-hover); background: var(--bp-surface-hover);
border-color: var(--bp-primary); border-color: var(--bp-primary);
box-shadow: var(--bp-focus-ring); box-shadow: var(--bp-focus-ring);
@@ -306,7 +307,8 @@ body.mouse-active {
color: var(--bp-text); color: var(--bp-text);
} }
.nav-item:focus { .nav-item:focus,
.nav-item.focused {
outline: none; outline: none;
background: var(--bp-surface-hover); background: var(--bp-surface-hover);
border-color: var(--bp-primary); border-color: var(--bp-primary);
@@ -314,7 +316,8 @@ body.mouse-active {
color: var(--bp-text); 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); transform: scale(1.1);
} }
@@ -449,7 +452,8 @@ body.mouse-active {
border-color: var(--bp-text-dim); border-color: var(--bp-text-dim);
} }
.action-btn:focus { .action-btn:focus,
.action-btn.focused {
outline: none; outline: none;
border-color: var(--bp-primary); border-color: var(--bp-primary);
box-shadow: var(--bp-focus-ring); box-shadow: var(--bp-focus-ring);
@@ -461,7 +465,8 @@ body.mouse-active {
} }
.action-btn.danger:hover, .action-btn.danger:hover,
.action-btn.danger:focus { .action-btn.danger:focus,
.action-btn.danger.focused {
border-color: var(--bp-danger); border-color: var(--bp-danger);
color: var(--bp-danger); color: var(--bp-danger);
} }
@@ -495,7 +500,8 @@ body.mouse-active {
} }
.search-card:focus, .search-card:focus,
.search-card:focus-within { .search-card:focus-within,
.search-card.focused {
outline: none; outline: none;
border-color: var(--bp-accent); border-color: var(--bp-accent);
box-shadow: var(--bp-focus-ring-accent); box-shadow: var(--bp-focus-ring-accent);
@@ -599,14 +605,16 @@ body.mouse-active {
opacity: 1; opacity: 1;
} }
.tile:focus { .tile:focus,
.tile.focused {
outline: none; outline: none;
border-color: var(--bp-primary); border-color: var(--bp-primary);
box-shadow: var(--bp-focus-ring); box-shadow: var(--bp-focus-ring);
transform: scale(1.02); transform: scale(1.02);
} }
.tile:focus::before { .tile:focus::before,
.tile.focused::before {
opacity: 1; opacity: 1;
} }
@@ -671,13 +679,15 @@ body.mouse-active {
} }
.tile.add-tile:hover, .tile.add-tile:hover,
.tile.add-tile:focus { .tile.add-tile:focus,
.tile.add-tile.focused {
border-color: var(--bp-accent); border-color: var(--bp-accent);
border-style: solid; border-style: solid;
} }
.tile.add-tile:hover .material-symbols-outlined, .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); color: var(--bp-accent);
} }
@@ -722,7 +732,8 @@ body.mouse-active {
transform: translateY(-4px); transform: translateY(-4px);
} }
.scroll-card:focus { .scroll-card:focus,
.scroll-card.focused {
outline: none; outline: none;
border-color: var(--bp-primary); border-color: var(--bp-primary);
box-shadow: var(--bp-focus-ring); box-shadow: var(--bp-focus-ring);
@@ -795,7 +806,8 @@ body.mouse-active {
background: var(--bp-surface-hover); background: var(--bp-surface-hover);
} }
.list-item:focus { .list-item:focus,
.list-item.focused {
outline: none; outline: none;
border-color: var(--bp-primary); border-color: var(--bp-primary);
box-shadow: var(--bp-focus-ring); box-shadow: var(--bp-focus-ring);
@@ -910,7 +922,8 @@ body.mouse-active {
transform: scale(1.02); transform: scale(1.02);
} }
.nebot-card:focus { .nebot-card:focus,
.nebot-card.focused {
outline: none; outline: none;
border-color: var(--bp-accent); border-color: var(--bp-accent);
box-shadow: var(--bp-focus-ring-accent); box-shadow: var(--bp-focus-ring-accent);
@@ -974,7 +987,8 @@ body.mouse-active {
transform: scale(1.02); transform: scale(1.02);
} }
.settings-card:focus { .settings-card:focus,
.settings-card.focused {
outline: none; outline: none;
border-color: var(--bp-primary); border-color: var(--bp-primary);
box-shadow: var(--bp-focus-ring); box-shadow: var(--bp-focus-ring);
@@ -1028,7 +1042,8 @@ body.mouse-active {
color: var(--bp-text); color: var(--bp-text);
} }
.settings-tab:focus { .settings-tab:focus,
.settings-tab.focused {
outline: none; outline: none;
border-color: var(--bp-primary); border-color: var(--bp-primary);
box-shadow: var(--bp-focus-ring); box-shadow: var(--bp-focus-ring);
@@ -1091,14 +1106,9 @@ body.mouse-active {
transform: translateY(-2px); transform: translateY(-2px);
} }
.theme-card:focus { .theme-card:focus,
outline: none;
border-color: var(--bp-primary);
box-shadow: var(--bp-focus-ring);
transform: translateY(-2px) scale(1.02);
}
.theme-card.focused { .theme-card.focused {
outline: none;
border-color: var(--bp-primary); border-color: var(--bp-primary);
box-shadow: var(--bp-focus-ring); box-shadow: var(--bp-focus-ring);
transform: translateY(-2px) scale(1.02); transform: translateY(-2px) scale(1.02);
@@ -1187,18 +1197,13 @@ body.mouse-active {
border-color: var(--bp-primary); border-color: var(--bp-primary);
} }
.scale-btn:focus { .scale-btn:focus,
.scale-btn.focused {
outline: none; outline: none;
border-color: var(--bp-primary); border-color: var(--bp-primary);
box-shadow: var(--bp-focus-ring); 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 { .scale-value {
min-width: 60px; min-width: 60px;
text-align: center; text-align: center;
@@ -1227,18 +1232,13 @@ body.mouse-active {
border-color: var(--bp-primary); border-color: var(--bp-primary);
} }
.action-button:focus { .action-button:focus,
.action-button.focused {
outline: none; outline: none;
border-color: var(--bp-primary); border-color: var(--bp-primary);
box-shadow: var(--bp-focus-ring); 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 { .action-button.danger:hover {
background: #dc3545; background: #dc3545;
border-color: #dc3545; border-color: #dc3545;
@@ -1500,7 +1500,8 @@ body.mouse-active {
} }
.osk-close:hover, .osk-close:hover,
.osk-close:focus { .osk-close:focus,
.osk-close.focused {
background: var(--bp-danger); background: var(--bp-danger);
border-color: var(--bp-danger); border-color: var(--bp-danger);
outline: none; outline: none;
@@ -1542,7 +1543,8 @@ body.mouse-active {
transform: scale(1.05); transform: scale(1.05);
} }
.osk-key:focus { .osk-key:focus,
.osk-key.focused {
outline: none; outline: none;
border-color: var(--bp-accent); border-color: var(--bp-accent);
box-shadow: 0 0 0 4px var(--bp-accent-glow), 0 0 20px var(--bp-accent-glow); 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:hover,
.osk-action-btn:focus { .osk-action-btn:focus,
.osk-action-btn.focused {
background: var(--bp-surface-active); background: var(--bp-surface-active);
outline: none; outline: none;
border-color: var(--bp-accent); border-color: var(--bp-accent);
@@ -1604,7 +1607,8 @@ body.mouse-active {
} }
.osk-action-btn.primary:hover, .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); box-shadow: var(--bp-focus-ring);
} }
@@ -1676,7 +1680,8 @@ body.mouse-active {
background: var(--bp-surface-hover); background: var(--bp-surface-hover);
} }
.context-item:focus { .context-item:focus,
.context-item.focused {
outline: none; outline: none;
border-color: var(--bp-primary); border-color: var(--bp-primary);
background: var(--bp-surface-hover); background: var(--bp-surface-hover);
@@ -1688,7 +1693,8 @@ body.mouse-active {
} }
/* Focus indicators for controller navigation */ /* Focus indicators for controller navigation */
[data-focusable]:focus { [data-focusable]:focus,
[data-focusable].focused {
outline: none; outline: none;
} }
+7 -2
View File
@@ -1912,10 +1912,15 @@ function navigateTo(url) {
webview.style.width = '100%'; webview.style.width = '100%';
webview.style.height = '100%'; webview.style.height = '100%';
webview.style.border = 'none'; 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.partition = 'persist:main';
webview.allowpopups = true; webview.allowpopups = true;
webview.webpreferences = 'allowRunningInsecureContent=false,javascript=true,webSecurity=true'; webview.setAttribute('webpreferences', 'allowRunningInsecureContent=false,javascript=true,webSecurity=true');
container.appendChild(webview); container.appendChild(webview);
state.currentWebview = webview; state.currentWebview = webview;
+6 -14
View File
@@ -22,6 +22,11 @@
color: rgba(255, 255, 255, 0.5); color: rgba(255, 255, 255, 0.5);
/* Adjust the color and transparency as needed */ /* Adjust the color and transparency as needed */
} }
#view-host {
flex: 1;
width: 100%;
}
</style> </style>
</head> </head>
<body> <body>
@@ -100,20 +105,7 @@
</div> </div>
<div id="webviews" class="hidden"></div> <div id="view-host"></div>
<!-- Home page container for direct loading -->
<div id="home-container" class="active">
<webview id="home-webview"
src="home.html"
preload="../preload.js"
partition="persist:main"
allowpopups
webpreferences="allowRunningInsecureContent=false,javascript=true,webSecurity=true"
useragent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 Nebula/1.0.0"
style="width:100%; height:100%; border:none;">
</webview>
</div>
<script src="script.js"></script> <script src="script.js"></script>
</body> </body>
+66
View File
@@ -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;
}
+23
View File
@@ -0,0 +1,23 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>Menu</title>
<link rel="stylesheet" href="menu-popup.css" />
</head>
<body>
<div id="menu-popup" role="menu">
<button data-cmd="open-settings" role="menuitem">Settings</button>
<button data-cmd="big-picture" role="menuitem">🎮 Big Picture Mode</button>
<button data-cmd="toggle-devtools" role="menuitem">Toggle Developer Tools</button>
<div class="zoom-controls" role="group" aria-label="Zoom controls">
<button data-cmd="zoom-out" aria-label="Zoom out">-</button>
<span id="zoom-percent">100%</span>
<button data-cmd="zoom-in" aria-label="Zoom in">+</button>
</div>
<button data-cmd="hard-reload" role="menuitem">Hard Reload (Ignore Cache)</button>
<button data-cmd="fresh-reload" role="menuitem">Reload Fresh (Add Cache-Buster)</button>
</div>
<script src="menu-popup.js"></script>
</body>
</html>
+46
View File
@@ -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();
+200 -519
View File
@@ -268,10 +268,25 @@ function applyThemeToMainUI(theme) {
// 1) cache hot DOM references // 1) cache hot DOM references
const urlBox = document.getElementById('url'); const urlBox = document.getElementById('url');
const tabBarEl = document.getElementById('tab-bar'); const tabBarEl = document.getElementById('tab-bar');
const webviewsEl = document.getElementById('webviews'); const viewHostEl = document.getElementById('view-host');
const menuPopup = document.getElementById('menu-popup'); const menuPopup = document.getElementById('menu-popup');
// (Removed old custom HTML context menu in favor of native Electron menu) // (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 // Select all text on focus and prevent mouseup from deselecting
urlBox.addEventListener('focus', () => { urlBox.addEventListener('focus', () => {
urlBox.select(); urlBox.select();
@@ -430,6 +445,71 @@ ipcRenderer.on('open-url-new-tab', (url) => {
if (typeof url === 'string' && url) createTab(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. // Auto-open on download start is disabled by design now.
function createTab(inputUrl) { function createTab(inputUrl) {
@@ -442,172 +522,27 @@ function createTab(inputUrl) {
pendingInternalNavigations.push(() => createTab(inputUrl)); pendingInternalNavigations.push(() => createTab(inputUrl));
return id; 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); let resolvedUrl = resolveInternalUrl(inputUrl);
console.log('[DEBUG] createTab resolvedUrl:', resolvedUrl, 'from inputUrl:', 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) // Keep data: URLs intact; BrowserView cannot consume blob URLs created in the UI process.
// 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 = `<html><body style="margin:0;background:#111;display:flex;align-items:center;justify-content:center;">`+
`<img src="${resolvedUrl}" style="max-width:100%;max-height:100%;object-fit:contain;"/>`+
`</body></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);
tabs.push({ tabs.push({
id, id,
url: inputUrl, // ← save the original input like "nebula://home" url: inputUrl,
title: 'New Tab', title: 'New Tab',
favicon: null, favicon: null,
history: [inputUrl], history: [inputUrl],
historyIndex: 0 historyIndex: 0
}); });
setActiveTab(id); ipcRenderer.invoke('browserview-create', { tabId: id, url: resolvedUrl })
.then(() => {
setActiveTab(id);
updateBrowserViewBounds();
})
.catch(() => {});
scheduleRenderTabs(); scheduleRenderTabs();
return id;
} }
// Expose for plugin usage (e.g., Nebot panel "Open Page") // 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); console.log('[DEBUG] Resolved plugin page', page, '->', resolved);
return resolved + suffix; 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); console.log('[DEBUG] Using fallback for page:', page);
if (page === 'nebot') return 'nebot.html' + suffix; const rel = `${page}.html${suffix}`;
return `${page}.html${suffix}`; try {
return new URL(rel, window.location.href).toString();
} catch {
return rel;
}
} }
console.log('[DEBUG] Page not in allowedInternalPages, returning 404'); 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:// // Allow direct loading of common schemes without forcing https://
if (/^(https?:|file:|data:|blob:)/i.test(url)) return url; if (/^(https?:|file:|data:|blob:)/i.test(url)) return url;
@@ -662,8 +605,11 @@ function resolveInternalUrl(url) {
function handleLoadFail(tabId) { function handleLoadFail(tabId) {
return (event) => { return (event) => {
if (!event.validatedURL.includes('nebula://') && event.errorCode !== -3) { if (!event.validatedURL.includes('nebula://') && event.errorCode !== -3) {
const webview = document.getElementById(`tab-${tabId}`); const badUrl = tabs.find(t => t.id === tabId)?.url || '';
webview.src = `404.html?url=${encodeURIComponent(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); 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) // Intercept plain HTTP (not HTTPS) navigations (excluding localhost / 127.* / internal pages)
try { try {
@@ -711,40 +657,12 @@ function performNavigation(input, originalInputForHistory) {
if (!isLoopback && !insecureBypassedHosts.has(host)) { if (!isLoopback && !insecureBypassedHosts.has(host)) {
const encoded = encodeURIComponent(resolved); const encoded = encodeURIComponent(resolved);
// Directly load insecure.html (avoid custom scheme so OS doesn't try to resolve an external handler) // Directly load insecure.html (avoid custom scheme so OS doesn't try to resolve an external handler)
const interstitial = `insecure.html?target=${encoded}`; resolved = `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;
} }
} }
} catch (e) { debug('[DEBUG] HTTP interception error', e); } } catch (e) { debug('[DEBUG] HTTP interception error', e); }
if (tab.isHome && !isInternal) { if (!activeTabId) {
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');
createTab(input); createTab(input);
return; return;
} }
@@ -752,7 +670,7 @@ function performNavigation(input, originalInputForHistory) {
tab.history.push(originalInputForHistory); tab.history.push(originalInputForHistory);
tab.historyIndex++; tab.historyIndex++;
tab.url = originalInputForHistory; tab.url = originalInputForHistory;
webview.src = resolved; ipcRenderer.invoke('browserview-load-url', { tabId: activeTabId, url: resolved }).catch(() => {});
scheduleRenderTabs(); scheduleRenderTabs();
scheduleUpdateNavButtons(); 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) { function handleNavigation(tabId, newUrl) {
const tab = tabs.find(t => t.id === tabId); const tab = tabs.find(t => t.id === tabId);
@@ -980,36 +791,16 @@ function handleNavigation(tabId, newUrl) {
function setActiveTab(id) { function setActiveTab(id) {
// hide all individual webviews activeTabId = id;
tabs.forEach(t => { ipcRenderer.invoke('browserview-set-active', { tabId: id }).catch(() => {});
const w = document.getElementById(`tab-${t.id}`); updateBrowserViewBounds();
if (w) w.classList.remove('active');
});
// toggle containers
const homeContainer = document.getElementById('home-container');
const webviewsEl = document.getElementById('webviews');
const tab = tabs.find(t => t.id === id); const tab = tabs.find(t => t.id === id);
if (tab) { 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; urlBox.value = tab.url === 'nebula://home' ? '' : tab.url;
scheduleRenderTabs(); scheduleRenderTabs();
updateNavButtons(); 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) ? (tabs[idx - 1]?.id ?? tabs[idx + 1]?.id ?? tabs[0]?.id)
: activeTabId; : activeTabId;
btn.addEventListener('animationend', () => { btn.addEventListener('animationend', () => {
// Remove webview ipcRenderer.invoke('browserview-destroy', { tabId: id }).catch(() => {});
const w = document.getElementById(`tab-${id}`);
if (w) w.remove();
// Remove from model // Remove from model
tabs = tabs.filter(t => t.id !== id); tabs = tabs.filter(t => t.id !== id);
// Choose a new active tab if needed // Choose a new active tab if needed
@@ -1039,8 +828,7 @@ function closeTab(id) {
return; return;
} }
// Fallback (no button rendered yet) // Fallback (no button rendered yet)
const w = document.getElementById(`tab-${id}`); ipcRenderer.invoke('browserview-destroy', { tabId: id }).catch(() => {});
if (w) w.remove();
tabs = tabs.filter(t => t.id !== id); tabs = tabs.filter(t => t.id !== id);
if (id === activeTabId && tabs.length > 0) setActiveTab(tabs[0].id); if (id === activeTabId && tabs.length > 0) setActiveTab(tabs[0].id);
scheduleRenderTabs(); scheduleRenderTabs();
@@ -1259,9 +1047,11 @@ function renderTabs() {
// 1) handle URL sent by main for a detached window // 1) handle URL sent by main for a detached window
ipcRenderer.on('open-url', (url) => { ipcRenderer.on('open-url', (url) => {
for (const t of tabs) {
ipcRenderer.invoke('browserview-destroy', { tabId: t.id }).catch(() => {});
}
tabs = []; tabs = [];
activeTabId = null; activeTabId = null;
webviewsEl.innerHTML = '';
tabBarEl.innerHTML = ''; tabBarEl.innerHTML = '';
if (typeof url === 'string' && url) createTab(url); else createTab(); if (typeof url === 'string' && url) createTab(url); else createTab();
}); });
@@ -1274,40 +1064,9 @@ function goBack() {
if (tab.historyIndex > 0) { if (tab.historyIndex > 0) {
tab.historyIndex--; tab.historyIndex--;
const targetUrl = tab.history[tab.historyIndex]; const targetUrl = tab.history[tab.historyIndex];
isHistoryNavigation = true;
// Special handling for nebula://home - convert webview tab back to home tab const resolvedUrl = resolveInternalUrl(targetUrl);
if (targetUrl === 'nebula://home') { ipcRenderer.invoke('browserview-load-url', { tabId: activeTabId, url: resolvedUrl }).catch(() => {});
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);
}
} }
} }
@@ -1319,48 +1078,9 @@ function goForward() {
if (tab.historyIndex < tab.history.length - 1) { if (tab.historyIndex < tab.history.length - 1) {
tab.historyIndex++; tab.historyIndex++;
const targetUrl = tab.history[tab.historyIndex]; const targetUrl = tab.history[tab.historyIndex];
isHistoryNavigation = true;
// Special handling for nebula://home - it doesn't use a webview const resolvedUrl = resolveInternalUrl(targetUrl);
if (targetUrl === 'nebula://home') { ipcRenderer.invoke('browserview-load-url', { tabId: activeTabId, url: resolvedUrl }).catch(() => {});
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);
}
} }
} }
@@ -1376,35 +1096,29 @@ function updateNavButtons() {
} }
function reload() { function reload() {
const webview = document.getElementById(`tab-${activeTabId}`); if (!activeTabId) return;
if (webview) { ipcRenderer.invoke('browserview-reload', { tabId: activeTabId, ignoreCache: false }).catch(() => {});
webview.reload(); scheduleUpdateNavButtons();
scheduleUpdateNavButtons(); // keep back/forward buttons in sync after a reload
}
} }
function hardReload() { function hardReload() {
const webview = document.getElementById(`tab-${activeTabId}`); if (!activeTabId) return;
if (webview && typeof webview.reloadIgnoringCache === 'function') { ipcRenderer.invoke('browserview-reload', { tabId: activeTabId, ignoreCache: true }).catch(() => {});
webview.reloadIgnoringCache(); scheduleUpdateNavButtons();
scheduleUpdateNavButtons();
} else if (webview) {
// Fallback
webview.reload();
}
} }
function freshReload() { function freshReload() {
const webview = document.getElementById(`tab-${activeTabId}`); if (!activeTabId) return;
if (!webview) return; ipcRenderer.invoke('browserview-get-url', { tabId: activeTabId }).then((currentUrl) => {
try { if (!currentUrl) return hardReload();
const u = new URL(webview.getURL()); try {
u.searchParams.set('_bust', Date.now().toString()); const u = new URL(currentUrl);
webview.src = u.toString(); u.searchParams.set('_bust', Date.now().toString());
} catch { ipcRenderer.invoke('browserview-load-url', { tabId: activeTabId, url: u.toString() }).catch(() => {});
// If URL parsing fails (e.g., internal pages), fall back to hard reload } catch {
hardReload(); hardReload();
} }
});
} }
// Function to open the Settings page // 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 // Open/close on button click; stop propagation so outside-click handler doesn't immediately close it
menuBtn.addEventListener('click', (e) => { menuBtn.addEventListener('click', (e) => {
e.stopPropagation(); e.stopPropagation();
menuPopup.classList.toggle('hidden'); if (!menuBtn) return;
if (!menuPopup.classList.contains('hidden')) { const rect = menuBtn.getBoundingClientRect();
updateZoomUI(); // ← refresh zoom % whenever menu opens 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
// 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');
}
}); });
// Close on Escape key // Close on Escape key
document.addEventListener('keydown', (e) => { document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && menuPopup && !menuPopup.classList.contains('hidden')) { if (e.key === 'Escape') ipcRenderer.send('menu-popup-hide');
menuPopup.classList.add('hidden');
}
if (e.key === 'Escape' && downloadsPopupEl && !downloadsPopupEl.classList.contains('hidden')) { if (e.key === 'Escape' && downloadsPopupEl && !downloadsPopupEl.classList.contains('hidden')) {
hideDownloadsPopup(); hideDownloadsPopup();
} }
}); });
// Also close when interacting with main content areas (covers webview clicks) // Close menus when BrowserView receives focus
const homeContainerEl = document.getElementById('home-container'); ipcRenderer.on('browserview-event', (payload) => {
if (webviewsEl) { if (!payload || !payload.type) return;
webviewsEl.addEventListener('pointerdown', () => { const { tabId, type } = payload;
if (!menuPopup.classList.contains('hidden')) menuPopup.classList.add('hidden'); if (type === 'focus') {
}); ipcRenderer.send('menu-popup-hide');
} if (downloadsPopupEl && !downloadsPopupEl.classList.contains('hidden')) hideDownloadsPopup();
if (homeContainerEl) { return;
homeContainerEl.addEventListener('pointerdown', () => { }
if (!menuPopup.classList.contains('hidden')) menuPopup.classList.add('hidden'); 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', () => { window.addEventListener('DOMContentLoaded', () => {
// Initialize theme from localStorage // Initialize theme from localStorage
@@ -1480,18 +1210,7 @@ window.addEventListener('DOMContentLoaded', () => {
try { try {
const theme = JSON.parse(savedTheme); const theme = JSON.parse(savedTheme);
applyThemeToMainUI(theme); applyThemeToMainUI(theme);
// Also send to home-webview once it's ready ipcRenderer.send('browserview-broadcast', { channel: 'theme-update', args: [theme] });
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 });
}
} catch (err) { } catch (err) {
console.error('Error applying saved theme:', err); console.error('Error applying saved theme:', err);
} }
@@ -1517,43 +1236,7 @@ window.addEventListener('DOMContentLoaded', () => {
// Initial boot // Initial boot
createTab(); createTab();
// Handle IPC messages from the static home webview (bookmarks navigation) updateBrowserViewBounds();
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);
}
}
});
// Fallback: listen for postMessage navigations from embedded pages (home/settings) // Fallback: listen for postMessage navigations from embedded pages (home/settings)
window.addEventListener('message', (event) => { window.addEventListener('message', (event) => {
if (event.data && event.data.type === 'navigate' && event.data.url) { if (event.data && event.data.type === 'navigate' && event.data.url) {
@@ -1593,8 +1276,8 @@ window.addEventListener('DOMContentLoaded', () => {
bigPictureBtn.addEventListener('click', async () => { bigPictureBtn.addEventListener('click', async () => {
try { try {
await window.bigPictureAPI.launch(); await window.bigPictureAPI.launch();
// Close the menu popup // Close the overlay menu
if (menuPopup) menuPopup.classList.add('hidden'); ipcRenderer.send('menu-popup-hide');
} catch (e) { } catch (e) {
console.error('Failed to launch Big Picture Mode:', e); console.error('Failed to launch Big Picture Mode:', e);
} }
@@ -1779,9 +1462,7 @@ try {
function attachCloseMenuOnInteract(el) { function attachCloseMenuOnInteract(el) {
if (!el) return; if (!el) return;
const closeIfOpen = () => { const closeIfOpen = () => {
if (menuPopup && !menuPopup.classList.contains('hidden')) { ipcRenderer.send('menu-popup-hide');
menuPopup.classList.add('hidden');
}
if (downloadsPopupEl && !downloadsPopupEl.classList.contains('hidden')) { if (downloadsPopupEl && !downloadsPopupEl.classList.contains('hidden')) {
hideDownloadsPopup(); 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 // For blob: URLs we need to resolve inside the active webview by converting to dataURL
if (url.startsWith('blob:')) { if (url.startsWith('blob:')) {
const webview = document.getElementById(`tab-${activeTabId}`); if (activeTabId) {
if (webview) { 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;}})();`;
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=>{ ipcRenderer.invoke('browserview-execute-js', { tabId: activeTabId, code }).then(dataUrl => {
if (dataUrl) { if (dataUrl) {
window.electronAPI.saveImageToDisk('image', dataUrl); window.electronAPI.saveImageToDisk('image', dataUrl);
} }