2026-03-20 19:50:50 +02:00
|
|
|
import { app, BrowserWindow, shell, ipcMain, dialog, Menu } from 'electron';
|
2026-03-17 16:22:14 +02:00
|
|
|
import path from 'path';
|
2026-03-19 20:20:26 +02:00
|
|
|
import { autoUpdater } from 'electron-updater';
|
2026-03-20 19:44:48 +02:00
|
|
|
import { registerGlobalErrorHandlers, showErrorDialog } from './modules/errors';
|
2026-03-20 19:48:35 +02:00
|
|
|
import { startNextServer, getServerPort } from './modules/server';
|
2026-03-20 19:50:50 +02:00
|
|
|
import { createMainWindow, getMainWindow } from './modules/window';
|
2026-03-17 16:22:14 +02:00
|
|
|
|
2026-03-20 19:44:48 +02:00
|
|
|
registerGlobalErrorHandlers();
|
2026-03-18 19:49:50 +02:00
|
|
|
|
2026-03-17 16:22:14 +02:00
|
|
|
const isDev = !app.isPackaged;
|
2026-03-17 17:32:28 +02:00
|
|
|
let deepLinkUrl: string | null = null;
|
2026-03-17 16:22:14 +02:00
|
|
|
|
2026-03-17 17:32:28 +02:00
|
|
|
const PROTOCOL = 'surfsense';
|
2026-03-17 16:22:14 +02:00
|
|
|
|
|
|
|
|
// IPC handlers
|
|
|
|
|
ipcMain.on('open-external', (_event, url: string) => {
|
2026-03-18 20:58:49 +02:00
|
|
|
try {
|
|
|
|
|
const parsed = new URL(url);
|
|
|
|
|
if (parsed.protocol === 'http:' || parsed.protocol === 'https:') {
|
|
|
|
|
shell.openExternal(url);
|
|
|
|
|
}
|
|
|
|
|
} catch {
|
|
|
|
|
// invalid URL — ignore
|
|
|
|
|
}
|
2026-03-17 16:22:14 +02:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
ipcMain.handle('get-app-version', () => {
|
|
|
|
|
return app.getVersion();
|
|
|
|
|
});
|
|
|
|
|
|
2026-03-17 17:32:28 +02:00
|
|
|
// Deep link handling
|
|
|
|
|
function handleDeepLink(url: string) {
|
|
|
|
|
if (!url.startsWith(`${PROTOCOL}://`)) return;
|
|
|
|
|
|
|
|
|
|
deepLinkUrl = url;
|
|
|
|
|
|
2026-03-20 19:50:50 +02:00
|
|
|
const win = getMainWindow();
|
|
|
|
|
if (!win) return;
|
2026-03-17 17:32:28 +02:00
|
|
|
|
|
|
|
|
const parsed = new URL(url);
|
|
|
|
|
if (parsed.hostname === 'auth' && parsed.pathname === '/callback') {
|
|
|
|
|
const params = parsed.searchParams.toString();
|
2026-03-20 19:50:50 +02:00
|
|
|
win.loadURL(`http://localhost:${getServerPort()}/auth/callback?${params}`);
|
2026-03-17 17:32:28 +02:00
|
|
|
}
|
|
|
|
|
|
2026-03-20 19:50:50 +02:00
|
|
|
win.show();
|
|
|
|
|
win.focus();
|
2026-03-17 17:32:28 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Single instance lock — second instance passes deep link to first
|
|
|
|
|
const gotTheLock = app.requestSingleInstanceLock();
|
|
|
|
|
if (!gotTheLock) {
|
|
|
|
|
app.quit();
|
|
|
|
|
} else {
|
|
|
|
|
app.on('second-instance', (_event, argv) => {
|
|
|
|
|
// Windows/Linux: deep link URL is in argv
|
|
|
|
|
const url = argv.find((arg) => arg.startsWith(`${PROTOCOL}://`));
|
|
|
|
|
if (url) handleDeepLink(url);
|
|
|
|
|
|
2026-03-20 19:50:50 +02:00
|
|
|
const win = getMainWindow();
|
|
|
|
|
if (win) {
|
|
|
|
|
if (win.isMinimized()) win.restore();
|
|
|
|
|
win.focus();
|
2026-03-17 17:32:28 +02:00
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// macOS: deep link arrives via open-url event
|
|
|
|
|
app.on('open-url', (event, url) => {
|
|
|
|
|
event.preventDefault();
|
|
|
|
|
handleDeepLink(url);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Register surfsense:// protocol
|
|
|
|
|
if (process.defaultApp) {
|
|
|
|
|
if (process.argv.length >= 2) {
|
|
|
|
|
app.setAsDefaultProtocolClient(PROTOCOL, process.execPath, [path.resolve(process.argv[1])]);
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
app.setAsDefaultProtocolClient(PROTOCOL);
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-19 20:20:26 +02:00
|
|
|
function setupAutoUpdater() {
|
|
|
|
|
if (isDev) return;
|
|
|
|
|
|
|
|
|
|
autoUpdater.autoDownload = true;
|
|
|
|
|
|
|
|
|
|
autoUpdater.on('update-available', (info) => {
|
|
|
|
|
console.log(`Update available: ${info.version}`);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
autoUpdater.on('update-downloaded', (info) => {
|
|
|
|
|
console.log(`Update downloaded: ${info.version}`);
|
|
|
|
|
dialog.showMessageBox({
|
|
|
|
|
type: 'info',
|
|
|
|
|
buttons: ['Restart', 'Later'],
|
|
|
|
|
defaultId: 0,
|
|
|
|
|
title: 'Update Ready',
|
|
|
|
|
message: `Version ${info.version} has been downloaded. Restart to apply the update.`,
|
|
|
|
|
}).then(({ response }) => {
|
|
|
|
|
if (response === 0) {
|
|
|
|
|
autoUpdater.quitAndInstall();
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
autoUpdater.on('error', (err) => {
|
|
|
|
|
console.error('Auto-updater error:', err);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
autoUpdater.checkForUpdates();
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-18 20:10:30 +02:00
|
|
|
function setupMenu() {
|
|
|
|
|
const isMac = process.platform === 'darwin';
|
|
|
|
|
const template: Electron.MenuItemConstructorOptions[] = [
|
|
|
|
|
...(isMac ? [{ role: 'appMenu' as const }] : []),
|
|
|
|
|
{ role: 'fileMenu' as const },
|
|
|
|
|
{ role: 'editMenu' as const },
|
|
|
|
|
{ role: 'viewMenu' as const },
|
|
|
|
|
{ role: 'windowMenu' as const },
|
|
|
|
|
];
|
|
|
|
|
Menu.setApplicationMenu(Menu.buildFromTemplate(template));
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-17 16:22:14 +02:00
|
|
|
// App lifecycle
|
|
|
|
|
app.whenReady().then(async () => {
|
2026-03-18 20:10:30 +02:00
|
|
|
setupMenu();
|
2026-03-18 19:49:59 +02:00
|
|
|
try {
|
|
|
|
|
await startNextServer();
|
|
|
|
|
} catch (error) {
|
|
|
|
|
showErrorDialog('Failed to start SurfSense', error);
|
|
|
|
|
setTimeout(() => app.quit(), 0);
|
|
|
|
|
return;
|
|
|
|
|
}
|
2026-03-20 19:50:50 +02:00
|
|
|
createMainWindow();
|
2026-03-19 20:20:26 +02:00
|
|
|
setupAutoUpdater();
|
2026-03-17 16:22:14 +02:00
|
|
|
|
2026-03-17 17:32:28 +02:00
|
|
|
// If a deep link was received before the window was ready, handle it now
|
|
|
|
|
if (deepLinkUrl) {
|
|
|
|
|
handleDeepLink(deepLinkUrl);
|
|
|
|
|
deepLinkUrl = null;
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-17 16:22:14 +02:00
|
|
|
app.on('activate', () => {
|
|
|
|
|
if (BrowserWindow.getAllWindows().length === 0) {
|
2026-03-20 19:50:50 +02:00
|
|
|
createMainWindow();
|
2026-03-17 16:22:14 +02:00
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
app.on('window-all-closed', () => {
|
|
|
|
|
if (process.platform !== 'darwin') {
|
|
|
|
|
app.quit();
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
app.on('will-quit', () => {
|
2026-03-18 17:51:47 +02:00
|
|
|
// Server runs in-process — no child process to kill
|
2026-03-17 16:22:14 +02:00
|
|
|
});
|