tray.ts : icône de barre système (Ouvrir, bascule « Launch at login », Quitter). autostart.ts : login items (Windows/macOS) + fichier ~/.config/autostart/*.desktop (Linux). updater.ts : electron-updater (provider generic -> release Gitea), no-op en dev, actif Windows/Linux. main.ts : fermeture de fenêtre = réduction dans le tray, quit explicite via tray/menu, arrêt propre du daemon avant sortie. icon.png ajouté aux extraResources (tray en packagé). Vérifié : typecheck contre l'API Electron + electron-updater, bundle esbuild.
128 lines
3.6 KiB
TypeScript
128 lines
3.6 KiB
TypeScript
import { app, BrowserWindow, session, shell, type BrowserWindowConstructorOptions, type Tray } from 'electron';
|
|
import { join } from 'node:path';
|
|
import { startDaemon, type DaemonHandle } from './daemon';
|
|
import { seedSessionCookie } from './auth';
|
|
import { loadWindowState, saveWindowState } from './window-state';
|
|
import { createTray } from './tray';
|
|
import { initUpdater } from './updater';
|
|
|
|
const PARTITION = 'persist:arboretum';
|
|
const PORT = 7317;
|
|
|
|
let daemon: DaemonHandle | null = null;
|
|
let win: BrowserWindow | null = null;
|
|
let tray: Tray | null = null;
|
|
let isQuitting = false;
|
|
let shuttingDown = false;
|
|
|
|
// Instance unique : deux instances = deux daemons/ports en conflit.
|
|
if (!app.requestSingleInstanceLock()) {
|
|
app.quit();
|
|
} else {
|
|
app.on('second-instance', showWindow);
|
|
app.whenReady().then(bootstrap).catch((err: unknown) => {
|
|
console.error('[arboretum-desktop] bootstrap failed:', err);
|
|
app.quit();
|
|
});
|
|
}
|
|
|
|
async function bootstrap(): Promise<void> {
|
|
const dataDir = join(app.getPath('userData'), 'daemon');
|
|
daemon = await startDaemon({ dataDir, port: PORT, onLog: (l) => process.stdout.write(l) });
|
|
await seedSessionCookie(PARTITION, daemon.url, daemon.token);
|
|
createWindow(daemon.url);
|
|
tray = createTray({ show: showWindow, quit: quitApp });
|
|
initUpdater();
|
|
}
|
|
|
|
function showWindow(): void {
|
|
if (!win) return;
|
|
if (win.isMinimized()) win.restore();
|
|
win.show();
|
|
win.focus();
|
|
}
|
|
|
|
function quitApp(): void {
|
|
isQuitting = true;
|
|
app.quit();
|
|
}
|
|
|
|
function createWindow(url: string): void {
|
|
const state = loadWindowState();
|
|
const opts: BrowserWindowConstructorOptions = {
|
|
width: state.width,
|
|
height: state.height,
|
|
backgroundColor: '#09090b',
|
|
webPreferences: {
|
|
partition: PARTITION,
|
|
contextIsolation: true,
|
|
sandbox: true,
|
|
nodeIntegration: false,
|
|
preload: join(__dirname, 'preload.js'),
|
|
},
|
|
};
|
|
if (state.x !== undefined) opts.x = state.x;
|
|
if (state.y !== undefined) opts.y = state.y;
|
|
|
|
win = new BrowserWindow(opts);
|
|
if (state.maximized) win.maximize();
|
|
|
|
// Liens externes -> navigateur système ; toute navigation hors origine locale est déviée.
|
|
win.webContents.setWindowOpenHandler(({ url: u }) => {
|
|
void shell.openExternal(u);
|
|
return { action: 'deny' };
|
|
});
|
|
win.webContents.on('will-navigate', (e, u) => {
|
|
if (!u.startsWith(url)) {
|
|
e.preventDefault();
|
|
void shell.openExternal(u);
|
|
}
|
|
});
|
|
|
|
// Permissions : n'autoriser que les notifications (cohérent avec la Permissions-Policy serveur).
|
|
session.fromPartition(PARTITION).setPermissionRequestHandler((_wc, permission, cb) => cb(permission === 'notifications'));
|
|
|
|
const persist = (): void => {
|
|
if (!win) return;
|
|
const b = win.getBounds();
|
|
saveWindowState({ width: b.width, height: b.height, x: b.x, y: b.y, maximized: win.isMaximized() });
|
|
};
|
|
|
|
// Fermer la fenêtre = réduire dans le tray (l'app continue en arrière-plan) ; quitter réellement
|
|
// se fait via le menu du tray ou le menu applicatif.
|
|
win.on('close', (e) => {
|
|
persist();
|
|
if (!isQuitting) {
|
|
e.preventDefault();
|
|
win?.hide();
|
|
}
|
|
});
|
|
win.on('closed', () => {
|
|
win = null;
|
|
});
|
|
|
|
void win.loadURL(`${url}/`);
|
|
}
|
|
|
|
app.on('before-quit', (e) => {
|
|
// Ne pas quitter avant l'arrêt propre du daemon enfant.
|
|
isQuitting = true;
|
|
if (daemon && !shuttingDown) {
|
|
e.preventDefault();
|
|
void shutdown();
|
|
}
|
|
});
|
|
|
|
async function shutdown(): Promise<void> {
|
|
if (shuttingDown) return;
|
|
shuttingDown = true;
|
|
try {
|
|
await daemon?.stop();
|
|
} finally {
|
|
daemon = null;
|
|
tray?.destroy();
|
|
tray = null;
|
|
app.quit();
|
|
}
|
|
}
|