SurfSense/surfsense_desktop/src/main.ts

161 lines
4 KiB
TypeScript
Raw Normal View History

import { app, BrowserWindow, shell, ipcMain, dialog, Menu } from 'electron';
import path from 'path';
import { autoUpdater } from 'electron-updater';
import { registerGlobalErrorHandlers, showErrorDialog } from './modules/errors';
import { startNextServer, getServerPort } from './modules/server';
import { createMainWindow, getMainWindow } from './modules/window';
registerGlobalErrorHandlers();
const isDev = !app.isPackaged;
let deepLinkUrl: string | null = null;
const PROTOCOL = 'surfsense';
// IPC handlers
ipcMain.on('open-external', (_event, url: string) => {
try {
const parsed = new URL(url);
if (parsed.protocol === 'http:' || parsed.protocol === 'https:') {
shell.openExternal(url);
}
} catch {
// invalid URL — ignore
}
});
ipcMain.handle('get-app-version', () => {
return app.getVersion();
});
// Deep link handling
function handleDeepLink(url: string) {
if (!url.startsWith(`${PROTOCOL}://`)) return;
deepLinkUrl = url;
const win = getMainWindow();
if (!win) return;
const parsed = new URL(url);
if (parsed.hostname === 'auth' && parsed.pathname === '/callback') {
const params = parsed.searchParams.toString();
win.loadURL(`http://localhost:${getServerPort()}/auth/callback?${params}`);
}
win.show();
win.focus();
}
// 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);
const win = getMainWindow();
if (win) {
if (win.isMinimized()) win.restore();
win.focus();
}
});
}
// 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);
}
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();
}
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));
}
// App lifecycle
app.whenReady().then(async () => {
setupMenu();
try {
await startNextServer();
} catch (error) {
showErrorDialog('Failed to start SurfSense', error);
setTimeout(() => app.quit(), 0);
return;
}
createMainWindow();
setupAutoUpdater();
// If a deep link was received before the window was ready, handle it now
if (deepLinkUrl) {
handleDeepLink(deepLinkUrl);
deepLinkUrl = null;
}
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) {
createMainWindow();
}
});
});
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit();
}
});
app.on('will-quit', () => {
// Server runs in-process — no child process to kill
});