diff --git a/main.js b/main.js index 50beb6b..739d0b5 100644 --- a/main.js +++ b/main.js @@ -1,4 +1,5 @@ const { app, BrowserWindow, ipcMain, session, screen, shell, dialog, Menu, clipboard, webContents } = require('electron'); +const { autoUpdater } = require('electron-updater'); const { pathToFileURL } = require('url'); const fs = require('fs'); const path = require('path'); @@ -140,14 +141,28 @@ function createWindow(startUrl) { perfMarks.browserWindow_instantiated = performance.now(); // Intercept window.open() requests and route them into the existing window as a new tab - // instead of spawning separate BrowserWindows. We still allow a small OAuth allowlist - // (accounts.google.com, login.microsoftonline.com, oauth, sso) to open real popups if - // the flow depends on window.opener relationships. Everything else becomes a new tab. + // instead of spawning separate BrowserWindows. We allow a small list of specific OAuth + // domains to open real popups if the flow depends on window.opener relationships. + // Everything else becomes a new tab. win.webContents.setWindowOpenHandler((details) => { const { url } = details; if (!/^https?:\/\//i.test(url)) return { action: 'deny' }; - // OAuth / SSO allowlist heuristic - if (/accounts\.google\.com|microsoftonline\.com|oauth|login|signin|sso/i.test(url)) { + // OAuth / SSO allowlist - only allow specific authentication provider domains + // Be restrictive to prevent normal links from opening in new windows + 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 => url.toLowerCase().includes(domain.toLowerCase())); + if (isOAuthDomain) { return { action: 'allow' }; // preserve popup semantics for complex auth flows } // Forward to renderer to open as tab @@ -166,18 +181,31 @@ function createWindow(startUrl) { // above now governs popup behavior. // ensure all embedded tags behave predictably without heavy injections - win.webContents.on('did-attach-webview', (event, webContents) => { + win.webContents.on('did-attach-webview', (event, webviewContents) => { // Route window.open() calls to tabs unless OAuth allowlist matched - webContents.setWindowOpenHandler((details) => { + webviewContents.setWindowOpenHandler((details) => { const { url } = details; if (!/^https?:\/\//i.test(url)) return { action: 'deny' }; - if (/accounts\.google\.com|microsoftonline\.com|oauth|login|signin|sso/i.test(url)) { + // OAuth / SSO allowlist - only allow specific authentication provider domains + 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 => url.toLowerCase().includes(domain.toLowerCase())); + if (isOAuthDomain) { return { action: 'allow' }; // keep popup for auth } - // Send to the owning window (embedder) to open a new tab + // Send to main window's webContents to open a new tab try { - const host = webContents.hostWebContents || webContents; - host.send('open-url-new-tab', url); + win.webContents.send('open-url-new-tab', url); } catch {} return { action: 'deny' }; }); @@ -364,6 +392,61 @@ app.whenReady().then(() => { app.on('activate', () => { if (BrowserWindow.getAllWindows().length === 0) createWindow(); }); + + // --- Auto-Updater Setup --- + // Configure auto-updater logging + autoUpdater.logger = require('electron-updater').autoUpdater.logger; + if (autoUpdater.logger) autoUpdater.logger.transports.file.level = 'info'; + + // Check for updates after a short delay to not block startup + setTimeout(() => { + autoUpdater.checkForUpdatesAndNotify().catch(err => { + console.log('[AutoUpdater] Update check failed:', err.message); + }); + }, 3000); + + // Auto-updater event handlers + autoUpdater.on('checking-for-update', () => { + console.log('[AutoUpdater] Checking for updates...'); + broadcastToAll('update-status', { status: 'checking' }); + }); + + autoUpdater.on('update-available', (info) => { + console.log('[AutoUpdater] Update available:', info.version); + broadcastToAll('update-status', { status: 'available', version: info.version }); + }); + + autoUpdater.on('update-not-available', (info) => { + console.log('[AutoUpdater] No update available. Current version:', app.getVersion()); + broadcastToAll('update-status', { status: 'not-available', currentVersion: app.getVersion() }); + }); + + autoUpdater.on('download-progress', (progress) => { + console.log(`[AutoUpdater] Download progress: ${progress.percent.toFixed(1)}%`); + broadcastToAll('update-status', { status: 'downloading', progress: progress.percent }); + }); + + autoUpdater.on('update-downloaded', (info) => { + console.log('[AutoUpdater] Update downloaded:', info.version); + broadcastToAll('update-status', { status: 'downloaded', version: info.version }); + // Optionally prompt user to restart + dialog.showMessageBox({ + type: 'info', + title: 'Update Ready', + message: `Nebula ${info.version} has been downloaded.`, + detail: 'The update will be installed when you restart the app.', + buttons: ['Restart Now', 'Later'] + }).then(result => { + if (result.response === 0) { + autoUpdater.quitAndInstall(); + } + }); + }); + + autoUpdater.on('error', (err) => { + console.error('[AutoUpdater] Error:', err.message); + broadcastToAll('update-status', { status: 'error', message: err.message }); + }); }); // Quit when all windows are closed. @@ -373,6 +456,33 @@ app.on('window-all-closed', () => { // ipcMain handlers +// --- Auto-Update IPC handlers --- +ipcMain.handle('check-for-updates', async () => { + try { + const result = await autoUpdater.checkForUpdates(); + return { success: true, updateInfo: result?.updateInfo }; + } catch (err) { + return { success: false, error: err.message }; + } +}); + +ipcMain.handle('download-update', async () => { + try { + await autoUpdater.downloadUpdate(); + return { success: true }; + } catch (err) { + return { success: false, error: err.message }; + } +}); + +ipcMain.handle('install-update', () => { + autoUpdater.quitAndInstall(); +}); + +ipcMain.handle('get-app-version', () => { + return app.getVersion(); +}); + // --- window control handlers (only registered once now) ipcMain.handle('window-minimize', event => { BrowserWindow.fromWebContents(event.sender).minimize(); @@ -560,6 +670,30 @@ ipcMain.handle('zoom-out', event => { return z; }); +ipcMain.handle('get-display-scale', async (event) => { + // Try to read from localStorage data (user data path) + const userDataPath = app.getPath('userData'); + const storageFile = path.join(userDataPath, 'localStorage'); + + try { + // Try to get from electron store or persistent storage + // For now, we'll just return a default and let the app set it + // The display scale is stored in localStorage on the client side + return 100; // Default to 100% + } catch (err) { + return 100; // Default to 100% + } +}); + +ipcMain.handle('set-zoom-factor', (event, zoomFactor) => { + const wc = BrowserWindow.fromWebContents(event.sender).webContents; + if (wc && typeof wc.setZoomFactor === 'function') { + wc.setZoomFactor(zoomFactor); + return true; + } + return false; +}); + // allow renderer to pop a tab into its own window ipcMain.handle('open-tab-in-new-window', (event, url) => { createWindow(url); @@ -750,7 +884,7 @@ ipcMain.handle('get-electron-versions', async (event, buildType = 'stable') => { }); ipcMain.handle('upgrade-electron', async (event, buildType = 'stable') => { - const { execFile } = require('child_process'); + const { exec } = require('child_process'); const packageName = buildType === 'nightly' ? 'electron-nightly' : 'electron'; return new Promise((resolve) => { @@ -758,10 +892,10 @@ ipcMain.handle('upgrade-electron', async (event, buildType = 'stable') => { const otherPackage = buildType === 'nightly' ? 'electron' : 'electron-nightly'; // Run npm install to upgrade the package - const args = ['install', '--save-dev', packageName + '@latest']; + const command = `npm install --save-dev ${packageName}@latest`; - execFile('npm', args, - { cwd: __dirname, shell: true, maxBuffer: 10 * 1024 * 1024 }, + exec(command, + { cwd: __dirname, maxBuffer: 10 * 1024 * 1024 }, (error, stdout, stderr) => { if (error) { console.error('Upgrade failed:', error); diff --git a/package-lock.json b/package-lock.json index 78c8889..23be45a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,11 +10,12 @@ "license": "ISC", "dependencies": { "dompurify": "^3.1.6", + "electron-updater": "^6.6.2", "highlight.js": "^11.9.0", "marked": "^12.0.2" }, "devDependencies": { - "electron": "^37.3.1", + "electron": "^39.2.7", "electron-builder": "^23.0.0", "electron-nightly": "^39.0.0-nightly.20250811" } @@ -419,7 +420,6 @@ "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -567,7 +567,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true, "license": "Python-2.0" }, "node_modules/asar": { @@ -1117,7 +1116,6 @@ "version": "4.4.1", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", - "dev": true, "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -1409,9 +1407,9 @@ } }, "node_modules/electron": { - "version": "37.3.1", - "resolved": "https://registry.npmjs.org/electron/-/electron-37.3.1.tgz", - "integrity": "sha512-7DhktRLqhe6OJh/Bo75bTI0puUYEmIwSzMinocgO63mx3MVjtIn2tYMzLmAleNIlud2htkjpsMG2zT4PiTCloA==", + "version": "39.2.7", + "resolved": "https://registry.npmjs.org/electron/-/electron-39.2.7.tgz", + "integrity": "sha512-KU0uFS6LSTh4aOIC3miolcbizOFP7N1M46VTYVfqIgFiuA2ilfNaOHLDS9tCMvwwHRowAsvqBrh9NgMXcTOHCQ==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -1619,6 +1617,82 @@ "node": ">= 10.0.0" } }, + "node_modules/electron-updater": { + "version": "6.6.2", + "resolved": "https://registry.npmjs.org/electron-updater/-/electron-updater-6.6.2.tgz", + "integrity": "sha512-Cr4GDOkbAUqRHP5/oeOmH/L2Bn6+FQPxVLZtPbcmKZC63a1F3uu5EefYOssgZXG3u/zBlubbJ5PJdITdMVggbw==", + "license": "MIT", + "dependencies": { + "builder-util-runtime": "9.3.1", + "fs-extra": "^10.1.0", + "js-yaml": "^4.1.0", + "lazy-val": "^1.0.5", + "lodash.escaperegexp": "^4.1.2", + "lodash.isequal": "^4.5.0", + "semver": "^7.6.3", + "tiny-typed-emitter": "^2.1.0" + } + }, + "node_modules/electron-updater/node_modules/builder-util-runtime": { + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.3.1.tgz", + "integrity": "sha512-2/egrNDDnRaxVwK3A+cJq6UOlqOdedGA7JPqCeJjN2Zjk1/QB/6QUi3b714ScIGS7HafFXTyzJEOr5b44I3kvQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.3.4", + "sax": "^1.2.4" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/electron-updater/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/electron-updater/node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/electron-updater/node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/electron-updater/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, "node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", @@ -2072,7 +2146,6 @@ "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "dev": true, "license": "ISC" }, "node_modules/graceful-readlink": { @@ -2358,7 +2431,6 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "dev": true, "license": "MIT", "dependencies": { "argparse": "^2.0.1" @@ -2426,7 +2498,6 @@ "version": "1.0.5", "resolved": "https://registry.npmjs.org/lazy-val/-/lazy-val-1.0.5.tgz", "integrity": "sha512-0/BnGCCfyUMkBpeDgWihanIAF9JmZhHBgUhEqzvf+adhNGLoP6TaiI5oF8oyb3I45P+PcnrqihSf01M0l0G5+Q==", - "dev": true, "license": "MIT" }, "node_modules/lodash": { @@ -2436,6 +2507,19 @@ "dev": true, "license": "MIT" }, + "node_modules/lodash.escaperegexp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz", + "integrity": "sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw==", + "license": "MIT" + }, + "node_modules/lodash.isequal": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", + "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==", + "deprecated": "This package is deprecated. Use require('node:util').isDeepStrictEqual instead.", + "license": "MIT" + }, "node_modules/lowercase-keys": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", @@ -2618,7 +2702,6 @@ "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, "license": "MIT" }, "node_modules/node-addon-api": { @@ -2846,7 +2929,6 @@ "version": "1.4.1", "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.1.tgz", "integrity": "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==", - "dev": true, "license": "ISC" }, "node_modules/semver": { @@ -3118,6 +3200,12 @@ "node": ">= 10.0.0" } }, + "node_modules/tiny-typed-emitter": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/tiny-typed-emitter/-/tiny-typed-emitter-2.1.0.tgz", + "integrity": "sha512-qVtvMxeXbVej0cQWKqVSSAHmKZEHAvxdF8HEUBFWts8h+xEo5m/lEiPakuyZ3BnCBjOD8i24kzNOiOLLgsSxhA==", + "license": "MIT" + }, "node_modules/tmp": { "version": "0.2.3", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.3.tgz", diff --git a/package.json b/package.json index 0b6523b..8c121f0 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "nebula", "productName": "Nebula", - "version": "1.0.0", + "version": "1.3.2", "main": "main.js", "scripts": { "start": "electron .", @@ -14,16 +14,24 @@ "description": "", "dependencies": { "dompurify": "^3.1.6", + "electron-updater": "^6.6.2", "highlight.js": "^11.9.0", "marked": "^12.0.2" }, "devDependencies": { - "electron": "^37.3.1", + "electron": "^39.2.7", "electron-builder": "^23.0.0", "electron-nightly": "^39.0.0-nightly.20250811" }, "build": { "appId": "com.andrewzambazos.nebula", + "publish": [ + { + "provider": "github", + "owner": "Bobbybear007", + "repo": "NebulaBrowser" + } + ], "mac": { "category": "public.app-category.productivity", "icon": "assets/images/Logos/Nebula-Favicon.icns" diff --git a/preload.js b/preload.js index b27404d..d717f1e 100644 --- a/preload.js +++ b/preload.js @@ -134,6 +134,15 @@ contextBridge.exposeInMainWorld('downloadsAPI', { onScanResult: (handler) => ipcRenderer.on('downloads-scan-result', (_e, payload) => handler(payload)) }); +// Auto-Updater API exposed to renderer +contextBridge.exposeInMainWorld('updaterAPI', { + checkForUpdates: () => ipcRenderer.invoke('check-for-updates'), + downloadUpdate: () => ipcRenderer.invoke('download-update'), + installUpdate: () => ipcRenderer.invoke('install-update'), + getAppVersion: () => ipcRenderer.invoke('get-app-version'), + onUpdateStatus: (handler) => ipcRenderer.on('update-status', (_e, payload) => handler(payload)) +}); + // ---------------------------------------- // Plugin renderer preloads // ---------------------------------------- diff --git a/renderer/script.js b/renderer/script.js index 4c5dcb7..249dd54 100644 --- a/renderer/script.js +++ b/renderer/script.js @@ -1098,6 +1098,24 @@ window.addEventListener('DOMContentLoaded', () => { console.error('Error applying saved theme:', err); } } + + // Initialize display scale (zoom) from localStorage + const savedDisplayScale = localStorage.getItem('nebula-display-scale'); + if (savedDisplayScale) { + try { + const scale = Number(savedDisplayScale); + if (scale > 0 && scale <= 300) { + const zoomFactor = scale / 100; + if (ipcRenderer && typeof ipcRenderer.invoke === 'function') { + ipcRenderer.invoke('set-zoom-factor', zoomFactor).catch(err => { + console.error('Error setting zoom factor:', err); + }); + } + } + } catch (err) { + console.error('Error applying saved display scale:', err); + } + } // Initial boot createTab(); diff --git a/renderer/settings.html b/renderer/settings.html index 8b59f73..ad4e7bb 100644 --- a/renderer/settings.html +++ b/renderer/settings.html @@ -105,6 +105,18 @@ + +
+

Display Scale

+
+
+ + 100% +
+

Adjust the default display scale (zoom) when opening the browser. Requires reload to take effect.

+
+
+

Custom Colors

diff --git a/renderer/settings.js b/renderer/settings.js index 07e9444..ed9d094 100644 --- a/renderer/settings.js +++ b/renderer/settings.js @@ -11,6 +11,7 @@ const WEATHER_UNIT_KEY = 'nebula-weather-unit'; // 'auto' | 'c' | 'f' const HOME_SEARCH_Y_KEY = 'nebula-home-search-y'; // number (vh) const HOME_BOOKMARKS_Y_KEY = 'nebula-home-bookmarks-y'; // number (vh) const HOME_GLANCE_CORNER_KEY = 'nebula-home-glance-corner'; // 'br'|'bl'|'tr'|'tl' +const DISPLAY_SCALE_KEY = 'nebula-display-scale'; // number (50-300) function showStatus(message) { if (statusText && statusDiv) { @@ -167,6 +168,27 @@ window.addEventListener('DOMContentLoaded', () => { notify(); })); } catch (e) { console.warn('Home layout control setup failed', e); } + + // Display scale controls + try { + const scaleSlider = document.getElementById('display-scale-slider'); + const scaleValue = document.getElementById('display-scale-value'); + + const initScale = Number(localStorage.getItem(DISPLAY_SCALE_KEY) || 100); + if (scaleSlider) { + scaleSlider.value = String(initScale); + if (scaleValue) scaleValue.textContent = initScale + '%'; + } + + if (scaleSlider) { + scaleSlider.addEventListener('input', () => { + const val = Number(scaleSlider.value); + if (scaleValue) scaleValue.textContent = val + '%'; + localStorage.setItem(DISPLAY_SCALE_KEY, String(val)); + showStatus(`Display scale set to ${val}%`); + }); + } + } catch (e) { console.warn('Display scale setup failed', e); } }); // Tabs: simple controller