Until now the desktop app only ran from a source checkout, so Pithos was really usable only inside Theseus. electron-builder (the version Theseus uses) now makes an NSIS installer and a portable exe; s3d is still fetched on first run by the checksum-checked installer, so the app stays small and never ships an s3d it did not verify. Icons are rendered from ui/icon.svg. Quitting also no longer waits on idle keep-alive sockets before exiting.
84 lines
2.9 KiB
JavaScript
84 lines
2.9 KiB
JavaScript
// Pithos desktop: an Electron window over the same control server the web
|
|
// console runs. The server binds a random loopback port and the window signs
|
|
// in with the one-time secret, so nothing else on the machine can drive it.
|
|
|
|
const { app, BrowserWindow, shell, Menu, dialog } = require('electron');
|
|
const path = require('node:path');
|
|
const { pathToFileURL } = require('node:url');
|
|
|
|
let pithos;
|
|
let win;
|
|
|
|
// Taskbar pins and notifications group under Pithos, not Electron.
|
|
if (process.platform === 'win32') app.setAppUserModelId('st.silentmode.pithos');
|
|
|
|
if (!app.requestSingleInstanceLock()) {
|
|
app.quit();
|
|
} else {
|
|
app.on('second-instance', () => {
|
|
if (win) { if (win.isMinimized()) win.restore(); win.focus(); }
|
|
});
|
|
app.whenReady().then(start).catch((err) => {
|
|
dialog.showErrorBox('Pithos failed to start', String(err?.stack || err));
|
|
app.quit();
|
|
});
|
|
}
|
|
|
|
async function start() {
|
|
const { createPithos } = await import(pathToFileURL(path.join(__dirname, '..', 'core', 'server.js')).href);
|
|
pithos = await createPithos({
|
|
host: '127.0.0.1',
|
|
port: 0,
|
|
hostName: 'desktop',
|
|
configFile: process.env.S3D_CONFIG_FILE || undefined,
|
|
// The pinned s3d release installs here, outside the (read-only) app bundle.
|
|
binDir: path.join(app.getPath('userData'), 'bin'),
|
|
openExternal: (url) => shell.openExternal(url),
|
|
});
|
|
|
|
Menu.setApplicationMenu(process.platform === 'darwin' ? Menu.buildFromTemplate([
|
|
{ role: 'appMenu' }, { role: 'editMenu' }, { role: 'viewMenu' }, { role: 'windowMenu' },
|
|
]) : null);
|
|
|
|
win = new BrowserWindow({
|
|
width: 1180,
|
|
height: 800,
|
|
minWidth: 380,
|
|
minHeight: 500,
|
|
title: 'Pithos',
|
|
backgroundColor: '#0f1214',
|
|
autoHideMenuBar: true,
|
|
webPreferences: { contextIsolation: true, nodeIntegration: false, sandbox: true },
|
|
});
|
|
|
|
const origin = new URL(pithos.url).origin;
|
|
// Keep the window on the console; anything else goes to the system browser.
|
|
win.webContents.on('will-navigate', (e, url) => {
|
|
if (new URL(url).origin !== origin) {
|
|
e.preventDefault();
|
|
if (url.startsWith('https://')) shell.openExternal(url);
|
|
}
|
|
});
|
|
win.webContents.setWindowOpenHandler(({ url }) => {
|
|
// HTML files opened as pages go to the system browser: the link carries
|
|
// its own ticket, and the page is sandboxed by the server.
|
|
const u = new URL(url);
|
|
if (u.origin === origin && u.pathname.startsWith('/page/')) shell.openExternal(url);
|
|
else if (url.startsWith('https://')) shell.openExternal(url);
|
|
return { action: 'deny' };
|
|
});
|
|
|
|
win.loadURL(pithos.authUrl);
|
|
win.on('closed', () => { win = null; });
|
|
}
|
|
|
|
let quitting = false;
|
|
app.on('before-quit', (e) => {
|
|
if (quitting || !pithos) return;
|
|
// Stop s3d cleanly so its upload buffer and database are flushed to disk.
|
|
e.preventDefault();
|
|
quitting = true;
|
|
pithos.close().finally(() => app.quit());
|
|
});
|
|
|
|
app.on('window-all-closed', () => app.quit());
|