SurfSense/surfsense_desktop/src/modules/window.ts

179 lines
5.2 KiB
TypeScript
Raw Normal View History

import { app, BrowserWindow, shell, session } from 'electron';
import path from 'path';
import { trackEvent } from './analytics';
import { showErrorDialog } from './errors';
import { getServerOrigin, getServerPort } from './server';
import { setActiveSearchSpaceId } from './active-search-space';
const isDev = !app.isPackaged;
const isMac = process.platform === 'darwin';
const WINDOW_TITLE = 'SurfSense';
function getHostedFrontendUrl(): string {
return (
process.env.SURFSENSE_HOSTED_FRONTEND_URL_OVERRIDE ||
process.env.HOSTED_FRONTEND_URL ||
'https://surfsense.net'
);
}
function getHostedFrontendHosts(): string[] {
try {
const host = new URL(getHostedFrontendUrl()).host;
const sibling = host.startsWith('www.') ? host.slice(4) : `www.${host}`;
return Array.from(new Set([host, sibling]));
} catch {
return [];
}
}
let mainWindow: BrowserWindow | null = null;
let isQuitting = false;
export function getMainWindow(): BrowserWindow | null {
return mainWindow;
}
// Called from main.ts on `before-quit` so the close-to-tray handler knows
// to actually let the window die instead of hiding it.
export function markQuitting(): void {
isQuitting = true;
}
export function createMainWindow(initialPath = '/dashboard'): BrowserWindow {
mainWindow = new BrowserWindow({
title: WINDOW_TITLE,
width: 1280,
height: 800,
minWidth: 800,
minHeight: 600,
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
contextIsolation: true,
nodeIntegration: false,
sandbox: true,
webviewTag: false,
},
show: false,
...(isMac
? {
titleBarStyle: 'hidden' as const,
trafficLightPosition: { x: 12, y: 10 },
}
: {}),
});
mainWindow.once('ready-to-show', () => {
mainWindow?.show();
});
mainWindow.webContents.on('page-title-updated', (event) => {
event.preventDefault();
mainWindow?.setTitle(WINDOW_TITLE);
});
mainWindow.webContents.on('did-finish-load', () => {
mainWindow?.setTitle(WINDOW_TITLE);
});
mainWindow.loadURL(`${getServerOrigin()}${initialPath}`);
mainWindow.webContents.setWindowOpenHandler(({ url }) => {
if (url.startsWith(getServerOrigin())) {
return { action: 'allow' };
}
shell.openExternal(url);
return { action: 'deny' };
});
const hostedHosts = getHostedFrontendHosts();
const rewriteFilter = {
urls: hostedHosts.flatMap((h) => [`http://${h}/*`, `https://${h}/*`]),
};
if (rewriteFilter.urls.length > 0) {
session.defaultSession.webRequest.onBeforeRequest(rewriteFilter, (details, callback) => {
try {
const u = new URL(details.url);
const originalHost = u.host;
const local = new URL(getServerOrigin());
u.protocol = local.protocol;
u.host = local.host;
trackEvent('desktop_oauth_redirect_intercepted', {
host: originalHost,
path: u.pathname,
rewritten_to_port: getServerPort(),
});
callback({ redirectURL: u.toString() });
} catch {
callback({});
}
});
}
// Diagnostic: connector callback landing somewhere other than localhost
// means the rewrite missed and the user is stranded off-app.
session.defaultSession.webRequest.onCompleted(
{ urls: ['*://*/dashboard/*/connectors/callback*'] },
(details) => {
try {
const u = new URL(details.url);
if (u.hostname === 'localhost' || u.hostname === '127.0.0.1') return;
trackEvent('desktop_oauth_redirect_missed', {
host: u.host,
path: u.pathname,
status_code: details.statusCode,
});
} catch {
// ignore malformed URLs
}
}
);
mainWindow.webContents.on('did-fail-load', (_event, errorCode, errorDescription, validatedURL) => {
console.error(`Failed to load ${validatedURL}: ${errorDescription} (${errorCode})`);
if (errorCode === -3) return;
showErrorDialog('Page failed to load', new Error(`${errorDescription} (${errorCode})\n${validatedURL}`));
});
// Auto-sync active search space from URL navigation
const syncSearchSpace = (url: string) => {
const match = url.match(/\/dashboard\/(\d+)/);
if (match) {
setActiveSearchSpaceId(match[1]);
}
};
mainWindow.webContents.on('did-navigate', (_event, url) => syncSearchSpace(url));
mainWindow.webContents.on('did-navigate-in-page', (_event, url) => syncSearchSpace(url));
if (isDev) {
mainWindow.webContents.openDevTools();
}
// Hide-to-tray on close (don't actually destroy the window unless the
// user really is quitting). Applies to every instance — including the one
// created lazily after a launch-at-login boot.
mainWindow.on('close', (e) => {
if (!isQuitting && mainWindow) {
e.preventDefault();
mainWindow.hide();
}
});
mainWindow.on('closed', () => {
mainWindow = null;
});
return mainWindow;
}
export function showMainWindow(source: 'tray_click' | 'tray_menu' | 'shortcut' = 'tray_click'): void {
const existing = getMainWindow();
const reopened = !existing || existing.isDestroyed();
if (reopened) {
createMainWindow('/dashboard');
} else {
existing.show();
existing.focus();
}
trackEvent('desktop_main_window_shown', { source, reopened });
}