diff --git a/main.js b/main.js index 44b274f..4124ba9 100644 --- a/main.js +++ b/main.js @@ -151,6 +151,77 @@ const GPUConfig = require('./gpu-config'); const PluginManager = require('./plugin-manager'); const portableData = require('./portable-data'); +// Windows: set explicit AppUserModelID to ensure proper default-app registration +// and notification branding. +if (process.platform === 'win32') { + try { + app.setAppUserModelId('com.andrewzambazos.nebula'); + } catch {} +} + +// --- Single instance + protocol URL handling --- +let pendingOpenUrl = null; + +function extractUrlFromArgv(argv = []) { + return argv.find(arg => /^https?:\/\//i.test(arg)); +} + +function openUrlInExistingWindow(targetUrl) { + if (!targetUrl) return false; + const windows = BrowserWindow.getAllWindows(); + const mainWindow = windows.find(w => { + try { return w && !w.isDestroyed() && !w.getParentWindow(); } catch { return false; } + }); + + if (mainWindow) { + try { mainWindow.show(); } catch {} + try { mainWindow.focus(); } catch {} + try { + mainWindow.webContents.send('open-url-new-tab', targetUrl); + return true; + } catch {} + try { + mainWindow.webContents.send('open-url', targetUrl); + return true; + } catch {} + } + + pendingOpenUrl = targetUrl; + return false; +} + +const gotSingleInstanceLock = app.requestSingleInstanceLock(); +if (!gotSingleInstanceLock) { + app.quit(); +} else { + app.on('second-instance', (_event, argv) => { + const url = extractUrlFromArgv(argv); + if (url) { + openUrlInExistingWindow(url); + return; + } + const windows = BrowserWindow.getAllWindows(); + const mainWindow = windows.find(w => { + try { return w && !w.isDestroyed() && !w.getParentWindow(); } catch { return false; } + }); + if (mainWindow) { + try { mainWindow.show(); } catch {} + try { mainWindow.focus(); } catch {} + } + }); +} + +app.on('open-url', (event, url) => { + event.preventDefault(); + openUrlInExistingWindow(url); +}); + +// Capture protocol URL if the app was launched with one +const initialProtocolUrl = extractUrlFromArgv(process.argv); +if (initialProtocolUrl) { + pendingOpenUrl = initialProtocolUrl; +} + // Initialize performance monitoring and GPU management const perfMonitor = new PerformanceMonitor(); const gpuFallback = new GPUFallback(); @@ -555,8 +626,21 @@ async function completeFirstRun(preferences = {}) { /** * Check if Nebula is set as the default browser */ +function getProtocolClientArgs() { + if (process.platform === 'win32' && process.defaultApp) { + const appPath = path.resolve(process.argv[1]); + return { exe: process.execPath, args: [appPath] }; + } + return null; +} + function isDefaultBrowser() { try { + const protocolArgs = getProtocolClientArgs(); + if (protocolArgs) { + return app.isDefaultProtocolClient('http', protocolArgs.exe, protocolArgs.args) + && app.isDefaultProtocolClient('https', protocolArgs.exe, protocolArgs.args); + } return app.isDefaultProtocolClient('http') && app.isDefaultProtocolClient('https'); } catch (err) { console.error('[DefaultBrowser] Error checking default browser status:', err); @@ -569,18 +653,42 @@ function isDefaultBrowser() { */ function setAsDefaultBrowser() { try { - const httpResult = app.setAsDefaultProtocolClient('http'); - const httpsResult = app.setAsDefaultProtocolClient('https'); - const htmlResult = app.setAsDefaultProtocolClient('html'); - - console.log('[DefaultBrowser] Set as default:', { httpResult, httpsResult, htmlResult }); - return httpResult && httpsResult; + const protocolArgs = getProtocolClientArgs(); + const httpResult = protocolArgs + ? app.setAsDefaultProtocolClient('http', protocolArgs.exe, protocolArgs.args) + : app.setAsDefaultProtocolClient('http'); + const httpsResult = protocolArgs + ? app.setAsDefaultProtocolClient('https', protocolArgs.exe, protocolArgs.args) + : app.setAsDefaultProtocolClient('https'); + const htmlResult = protocolArgs + ? app.setAsDefaultProtocolClient('html', protocolArgs.exe, protocolArgs.args) + : app.setAsDefaultProtocolClient('html'); + + const success = httpResult && httpsResult; + const needsUserAction = success && !isDefaultBrowser(); + + console.log('[DefaultBrowser] Set as default:', { httpResult, httpsResult, htmlResult, needsUserAction }); + return { success, needsUserAction }; } catch (err) { console.error('[DefaultBrowser] Error setting as default browser:', err); - return false; + return { success: false, needsUserAction: false, error: err.message }; } } +function openDefaultBrowserSettings() { + try { + if (process.platform === 'win32') { + return shell.openExternal('ms-settings:defaultapps'); + } + if (process.platform === 'darwin') { + return shell.openExternal('x-apple.systempreferences:com.apple.preference.general?DefaultWebBrowser'); + } + } catch (err) { + console.warn('[DefaultBrowser] Failed to open system settings:', err.message || err); + } + return false; +} + // ============================================================================= // Initialize portable data paths BEFORE app.ready (must be done early) @@ -1246,12 +1354,15 @@ app.whenReady().then(() => { // If launched via SteamOS Gaming Mode / gamepad UI, default to Big Picture Mode. // Desktop launches remain unchanged. Big Picture now opens in main window to keep resources low. - const startInBigPicture = shouldStartInBigPictureMode(); + const startUrl = pendingOpenUrl; + pendingOpenUrl = null; + + const startInBigPicture = startUrl ? false : shouldStartInBigPictureMode(); if (startInBigPicture) { console.log('[Startup] Detected game mode launch; starting in Big Picture Mode (in main window)'); createWindow(null, true); // Pass bigPictureMode flag } else { - createWindow(); + createWindow(startUrl || null, false); } // Initialize user plugins after app ready @@ -1464,13 +1575,23 @@ ipcMain.handle('is-default-browser', () => { ipcMain.handle('set-as-default-browser', () => { try { const result = setAsDefaultBrowser(); - return { success: result }; + return result; } catch (err) { console.error('[DefaultBrowser] Error in IPC handler:', err); return { success: false, error: err.message }; } }); +ipcMain.handle('open-default-browser-settings', () => { + try { + const result = openDefaultBrowserSettings(); + return { success: !!result }; + } catch (err) { + console.error('[DefaultBrowser] Error opening system settings:', err); + return { success: false, error: err.message }; + } +}); + // --- window control handlers (only registered once now) ipcMain.handle('window-minimize', event => { BrowserWindow.fromWebContents(event.sender).minimize(); diff --git a/package.json b/package.json index c5cdedb..06381bf 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,15 @@ }, "build": { "appId": "com.andrewzambazos.nebula", + "protocols": [ + { + "name": "Nebula", + "schemes": [ + "http", + "https" + ] + } + ], "publish": [ { "provider": "github", diff --git a/preload.js b/preload.js index 0cefaba..8eb4684 100644 --- a/preload.js +++ b/preload.js @@ -517,6 +517,8 @@ contextBridge.exposeInMainWorld('api', { isDefaultBrowser: () => ipcRenderer.invoke('is-default-browser'), // Set Nebula as the default browser setAsDefaultBrowser: () => ipcRenderer.invoke('set-as-default-browser'), + // Open OS default browser settings + openDefaultBrowserSettings: () => ipcRenderer.invoke('open-default-browser-settings'), // Complete first-run setup completeFirstRun: (data) => ipcRenderer.invoke('complete-first-run', data), // Get first-run data diff --git a/renderer/setup.js b/renderer/setup.js index a3d5e85..ba22562 100644 --- a/renderer/setup.js +++ b/renderer/setup.js @@ -294,26 +294,47 @@ async function setDefaultBrowser() { const result = await window.api.setAsDefaultBrowser(); if (result.success) { - setupState.defaultBrowserSet = true; - + const isDefault = await window.api.isDefaultBrowser(); + if (isDefault) { + setupState.defaultBrowserSet = true; + + if (statusEl) { + statusEl.classList.remove('not-default'); + statusEl.classList.add('is-default'); + statusEl.innerHTML = ` +
+

Nebula is now your default browser!

+ `; + } + + if (btn) { + btn.innerHTML = ' Set Successfully'; + } + + // Auto-advance after a brief delay + setTimeout(() => goToStep(4), 1500); + return; + } + if (statusEl) { statusEl.classList.remove('not-default'); - statusEl.classList.add('is-default'); statusEl.innerHTML = ` -
-

Nebula is now your default browser!

+
ℹ️
+

System settings opened. Choose Nebula as your default browser to finish.

`; } - + if (btn) { - btn.innerHTML = ' Set Successfully'; + btn.disabled = false; + btn.innerHTML = ' Check Again'; } - - // Auto-advance after a brief delay - setTimeout(() => goToStep(4), 1500); - } else { - throw new Error(result.error || 'Failed to set default browser'); + + if (result.needsUserAction && window.api.openDefaultBrowserSettings) { + try { await window.api.openDefaultBrowserSettings(); } catch {} + } + return; } + throw new Error(result.error || 'Failed to set default browser'); } catch (error) { console.error('[Setup] Error setting default browser:', error);