Improve default browser handling and protocol support

Adds explicit AppUserModelID for Windows, robust single-instance and protocol URL handling, and protocol registration for http/https in package.json. Enhances default browser logic to handle OS-specific requirements, provides user feedback in the setup UI, and allows opening system settings for default browser selection. Updates preload and renderer logic to support these changes.
This commit is contained in:
2026-01-21 21:11:59 +13:00
parent 97150ce2c4
commit 28d2daf06d
4 changed files with 175 additions and 22 deletions
+130 -9
View File
@@ -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');
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');
console.log('[DefaultBrowser] Set as default:', { httpResult, httpsResult, htmlResult });
return httpResult && httpsResult;
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();
+9
View File
@@ -28,6 +28,15 @@
},
"build": {
"appId": "com.andrewzambazos.nebula",
"protocols": [
{
"name": "Nebula",
"schemes": [
"http",
"https"
]
}
],
"publish": [
{
"provider": "github",
+2
View File
@@ -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
+23 -2
View File
@@ -294,6 +294,8 @@ async function setDefaultBrowser() {
const result = await window.api.setAsDefaultBrowser();
if (result.success) {
const isDefault = await window.api.isDefaultBrowser();
if (isDefault) {
setupState.defaultBrowserSet = true;
if (statusEl) {
@@ -311,9 +313,28 @@ async function setDefaultBrowser() {
// Auto-advance after a brief delay
setTimeout(() => goToStep(4), 1500);
} else {
throw new Error(result.error || 'Failed to set default browser');
return;
}
if (statusEl) {
statusEl.classList.remove('not-default');
statusEl.innerHTML = `
<div class="status-icon">️</div>
<p class="status-text">System settings opened. Choose Nebula as your default browser to finish.</p>
`;
}
if (btn) {
btn.disabled = false;
btn.innerHTML = '<span class="btn-icon">↻</span> Check Again';
}
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);