This commit is contained in:
@@ -0,0 +1,188 @@
|
||||
'use strict';
|
||||
|
||||
const path = require('node:path');
|
||||
const { pathToFileURL } = require('node:url');
|
||||
const { app, BrowserWindow, Menu, shell, session, ipcMain } = require('electron');
|
||||
const { validateBackendUrl } = require('./url-validation');
|
||||
|
||||
const BACKEND_URL_KEY = 'backendUrl';
|
||||
let backendUrl = '';
|
||||
let mainWindow = null;
|
||||
let settingsWindow = null;
|
||||
|
||||
function bundledUiPath() {
|
||||
return app.isPackaged
|
||||
? path.join(process.resourcesPath, 'web', 'index.html')
|
||||
: path.resolve(__dirname, '..', 'web', 'index.html');
|
||||
}
|
||||
|
||||
function configuredWebUrl() {
|
||||
const argument = process.argv.find((item) => item.startsWith('--web-url='));
|
||||
if (!argument) return null;
|
||||
const result = validateBackendUrl(argument.slice('--web-url='.length));
|
||||
return result.valid ? result.value : null;
|
||||
}
|
||||
|
||||
function loadableUi() {
|
||||
return configuredWebUrl() || pathToFileURL(bundledUiPath()).toString();
|
||||
}
|
||||
|
||||
function createWindowOptions(width = 1440, height = 900) {
|
||||
return {
|
||||
width,
|
||||
height,
|
||||
minWidth: 960,
|
||||
minHeight: 640,
|
||||
show: false,
|
||||
webPreferences: {
|
||||
preload: path.join(__dirname, 'preload.cjs'),
|
||||
contextIsolation: true,
|
||||
sandbox: true,
|
||||
nodeIntegration: false,
|
||||
navigateOnDragDrop: false
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function openExternalLink(event, url) {
|
||||
if (!/^https?:\/\//i.test(url)) return;
|
||||
event.preventDefault();
|
||||
void shell.openExternal(url);
|
||||
}
|
||||
|
||||
function wireExternalNavigation(window) {
|
||||
window.webContents.setWindowOpenHandler(({ url }) => {
|
||||
openExternalLink({ preventDefault() {} }, url);
|
||||
return { action: 'deny' };
|
||||
});
|
||||
window.webContents.on('will-navigate', (event, url) => {
|
||||
const current = window.webContents.getURL();
|
||||
if (url.startsWith('file://')) return;
|
||||
if (url.startsWith('http://') || url.startsWith('https://')) {
|
||||
const currentOrigin = current.startsWith('http') ? new URL(current).origin : '';
|
||||
if (currentOrigin === new URL(url).origin) return;
|
||||
}
|
||||
openExternalLink(event, url);
|
||||
});
|
||||
}
|
||||
|
||||
function injectBackendConfig(window) {
|
||||
const serialized = JSON.stringify(backendUrl);
|
||||
return window.webContents.executeJavaScript(
|
||||
`window.__PROSPECT_CONFIG__ = Object.freeze(Object.assign({}, window.__PROSPECT_CONFIG__ || {}, { apiBase: ${serialized} }));`,
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
async function loadMainUi() {
|
||||
if (!mainWindow) return;
|
||||
await mainWindow.loadURL(loadableUi());
|
||||
await injectBackendConfig(mainWindow);
|
||||
}
|
||||
|
||||
function createMainWindow() {
|
||||
mainWindow = new BrowserWindow(createWindowOptions());
|
||||
wireExternalNavigation(mainWindow);
|
||||
mainWindow.once('ready-to-show', () => mainWindow.show());
|
||||
mainWindow.on('closed', () => { mainWindow = null; });
|
||||
void loadMainUi().catch(() => mainWindow?.webContents.executeJavaScript('location.reload()'));
|
||||
return mainWindow;
|
||||
}
|
||||
|
||||
function createSettingsWindow() {
|
||||
if (settingsWindow && !settingsWindow.isDestroyed()) {
|
||||
settingsWindow.focus();
|
||||
return settingsWindow;
|
||||
}
|
||||
settingsWindow = new BrowserWindow({
|
||||
...createWindowOptions(560, 500),
|
||||
resizable: false,
|
||||
title: 'ProspectOS connection settings'
|
||||
});
|
||||
wireExternalNavigation(settingsWindow);
|
||||
settingsWindow.once('ready-to-show', () => settingsWindow.show());
|
||||
settingsWindow.on('closed', () => { settingsWindow = null; });
|
||||
void settingsWindow.loadFile(path.join(__dirname, 'connection.html'));
|
||||
return settingsWindow;
|
||||
}
|
||||
|
||||
function installMenu() {
|
||||
Menu.setApplicationMenu(Menu.buildFromTemplate([
|
||||
{ label: 'ProspectOS', submenu: [
|
||||
{ label: 'Connection settings…', click: () => createSettingsWindow() },
|
||||
{ type: 'separator' },
|
||||
{ label: 'Clear session and cache', click: () => clearSession() },
|
||||
{ role: 'quit' }
|
||||
] },
|
||||
{ label: 'View', submenu: [
|
||||
{ label: 'Reload', accelerator: 'CmdOrCtrl+R', click: () => mainWindow?.reload() },
|
||||
{ label: 'Reconnect', accelerator: 'CmdOrCtrl+Shift+R', click: () => reconnect() },
|
||||
{ role: 'toggleDevTools' }
|
||||
] }
|
||||
]));
|
||||
}
|
||||
|
||||
async function clearSession() {
|
||||
await session.defaultSession.clearStorageData();
|
||||
await session.defaultSession.clearCache();
|
||||
if (mainWindow && !mainWindow.isDestroyed()) await mainWindow.reload();
|
||||
}
|
||||
|
||||
async function reconnect() {
|
||||
if (!mainWindow || mainWindow.isDestroyed()) return;
|
||||
await mainWindow.webContents.session.clearCache();
|
||||
await loadMainUi();
|
||||
}
|
||||
|
||||
function registerIpc() {
|
||||
ipcMain.handle('backend:get', () => backendUrl);
|
||||
ipcMain.handle('backend:set', async (_event, value) => {
|
||||
const result = validateBackendUrl(value);
|
||||
if (!result.valid) return result;
|
||||
backendUrl = result.value;
|
||||
// This is the only persisted setting, and it is explicitly non-secret.
|
||||
const storePath = path.join(app.getPath('userData'), 'connection.json');
|
||||
require('node:fs').writeFileSync(storePath, JSON.stringify({ backendUrl }), { mode: 0o600 });
|
||||
require('node:fs').chmodSync(storePath, 0o600);
|
||||
if (mainWindow && !mainWindow.isDestroyed()) await loadMainUi();
|
||||
if (settingsWindow && !settingsWindow.isDestroyed()) settingsWindow.close();
|
||||
if (!mainWindow) createMainWindow();
|
||||
return { valid: true, value: backendUrl };
|
||||
});
|
||||
ipcMain.handle('window:reload', () => mainWindow?.reload());
|
||||
ipcMain.handle('window:reconnect', () => reconnect());
|
||||
ipcMain.handle('session:clear', () => clearSession());
|
||||
ipcMain.handle('settings:open', () => createSettingsWindow());
|
||||
}
|
||||
|
||||
function readStoredBackendUrl() {
|
||||
const argument = process.argv.find((item) => item.startsWith('--api-base='));
|
||||
const configured = argument ? argument.slice('--api-base='.length) : process.env.PROSPECT_API_BASE;
|
||||
if (configured) {
|
||||
const result = validateBackendUrl(configured);
|
||||
if (result.valid) return result.value;
|
||||
}
|
||||
try {
|
||||
const storePath = path.join(app.getPath('userData'), 'connection.json');
|
||||
const parsed = JSON.parse(require('node:fs').readFileSync(storePath, 'utf8'));
|
||||
const result = validateBackendUrl(parsed.backendUrl);
|
||||
return result.valid ? result.value : '';
|
||||
} catch { return ''; }
|
||||
}
|
||||
|
||||
app.whenReady().then(() => {
|
||||
backendUrl = readStoredBackendUrl();
|
||||
registerIpc();
|
||||
installMenu();
|
||||
if (backendUrl) createMainWindow();
|
||||
else createSettingsWindow();
|
||||
app.on('activate', () => {
|
||||
if (BrowserWindow.getAllWindows().length === 0) {
|
||||
if (backendUrl) createMainWindow(); else createSettingsWindow();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
app.on('window-all-closed', () => { if (process.platform !== 'darwin') app.quit(); });
|
||||
|
||||
module.exports = { createWindowOptions, loadableUi };
|
||||
Reference in New Issue
Block a user