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 PluginManager = require('./plugin-manager');
const portableData = require('./portable-data'); 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 // Initialize performance monitoring and GPU management
const perfMonitor = new PerformanceMonitor(); const perfMonitor = new PerformanceMonitor();
const gpuFallback = new GPUFallback(); const gpuFallback = new GPUFallback();
@@ -555,8 +626,21 @@ async function completeFirstRun(preferences = {}) {
/** /**
* Check if Nebula is set as the default browser * 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() { function isDefaultBrowser() {
try { 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'); return app.isDefaultProtocolClient('http') && app.isDefaultProtocolClient('https');
} catch (err) { } catch (err) {
console.error('[DefaultBrowser] Error checking default browser status:', err); console.error('[DefaultBrowser] Error checking default browser status:', err);
@@ -569,18 +653,42 @@ function isDefaultBrowser() {
*/ */
function setAsDefaultBrowser() { function setAsDefaultBrowser() {
try { try {
const httpResult = app.setAsDefaultProtocolClient('http'); const protocolArgs = getProtocolClientArgs();
const httpsResult = app.setAsDefaultProtocolClient('https'); const httpResult = protocolArgs
const htmlResult = app.setAsDefaultProtocolClient('html'); ? 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 }); const success = httpResult && httpsResult;
return httpResult && httpsResult; const needsUserAction = success && !isDefaultBrowser();
console.log('[DefaultBrowser] Set as default:', { httpResult, httpsResult, htmlResult, needsUserAction });
return { success, needsUserAction };
} catch (err) { } catch (err) {
console.error('[DefaultBrowser] Error setting as default browser:', 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) // 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. // 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. // 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) { if (startInBigPicture) {
console.log('[Startup] Detected game mode launch; starting in Big Picture Mode (in main window)'); console.log('[Startup] Detected game mode launch; starting in Big Picture Mode (in main window)');
createWindow(null, true); // Pass bigPictureMode flag createWindow(null, true); // Pass bigPictureMode flag
} else { } else {
createWindow(); createWindow(startUrl || null, false);
} }
// Initialize user plugins after app ready // Initialize user plugins after app ready
@@ -1464,13 +1575,23 @@ ipcMain.handle('is-default-browser', () => {
ipcMain.handle('set-as-default-browser', () => { ipcMain.handle('set-as-default-browser', () => {
try { try {
const result = setAsDefaultBrowser(); const result = setAsDefaultBrowser();
return { success: result }; return result;
} catch (err) { } catch (err) {
console.error('[DefaultBrowser] Error in IPC handler:', err); console.error('[DefaultBrowser] Error in IPC handler:', err);
return { success: false, error: err.message }; 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) // --- window control handlers (only registered once now)
ipcMain.handle('window-minimize', event => { ipcMain.handle('window-minimize', event => {
BrowserWindow.fromWebContents(event.sender).minimize(); BrowserWindow.fromWebContents(event.sender).minimize();
+9
View File
@@ -28,6 +28,15 @@
}, },
"build": { "build": {
"appId": "com.andrewzambazos.nebula", "appId": "com.andrewzambazos.nebula",
"protocols": [
{
"name": "Nebula",
"schemes": [
"http",
"https"
]
}
],
"publish": [ "publish": [
{ {
"provider": "github", "provider": "github",
+2
View File
@@ -517,6 +517,8 @@ contextBridge.exposeInMainWorld('api', {
isDefaultBrowser: () => ipcRenderer.invoke('is-default-browser'), isDefaultBrowser: () => ipcRenderer.invoke('is-default-browser'),
// Set Nebula as the default browser // Set Nebula as the default browser
setAsDefaultBrowser: () => ipcRenderer.invoke('set-as-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 // Complete first-run setup
completeFirstRun: (data) => ipcRenderer.invoke('complete-first-run', data), completeFirstRun: (data) => ipcRenderer.invoke('complete-first-run', data),
// Get first-run data // Get first-run data
+30 -9
View File
@@ -294,26 +294,47 @@ async function setDefaultBrowser() {
const result = await window.api.setAsDefaultBrowser(); const result = await window.api.setAsDefaultBrowser();
if (result.success) { 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 = `
<div class="status-icon">✓</div>
<p class="status-text">Nebula is now your default browser!</p>
`;
}
if (btn) {
btn.innerHTML = '<span class="btn-icon">✓</span> Set Successfully';
}
// Auto-advance after a brief delay
setTimeout(() => goToStep(4), 1500);
return;
}
if (statusEl) { if (statusEl) {
statusEl.classList.remove('not-default'); statusEl.classList.remove('not-default');
statusEl.classList.add('is-default');
statusEl.innerHTML = ` statusEl.innerHTML = `
<div class="status-icon"></div> <div class="status-icon"></div>
<p class="status-text">Nebula is now your default browser!</p> <p class="status-text">System settings opened. Choose Nebula as your default browser to finish.</p>
`; `;
} }
if (btn) { if (btn) {
btn.innerHTML = '<span class="btn-icon">✓</span> Set Successfully'; btn.disabled = false;
btn.innerHTML = '<span class="btn-icon">↻</span> Check Again';
} }
// Auto-advance after a brief delay if (result.needsUserAction && window.api.openDefaultBrowserSettings) {
setTimeout(() => goToStep(4), 1500); try { await window.api.openDefaultBrowserSettings(); } catch {}
} else { }
throw new Error(result.error || 'Failed to set default browser'); return;
} }
throw new Error(result.error || 'Failed to set default browser');
} catch (error) { } catch (error) {
console.error('[Setup] Error setting default browser:', error); console.error('[Setup] Error setting default browser:', error);