feat(desktop): tray + lancement au login + auto-update (C5)
Some checks failed
CI / Build & test (Node 24) (push) Has been cancelled
CI / Pack & boot smoke (Node 22) (push) Has been cancelled
CI / No em/en dashes (push) Has been cancelled
CI / Build & test (Node 22) (push) Has been cancelled

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.
This commit is contained in:
2026-07-17 18:04:36 +02:00
parent c9811fc0ca
commit ccbc1b9e4e
7 changed files with 238 additions and 8 deletions

View File

@@ -1,25 +1,25 @@
import { app, BrowserWindow, session, shell, type BrowserWindowConstructorOptions } from 'electron';
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', () => {
if (!win) return;
if (win.isMinimized()) win.restore();
win.focus();
});
app.on('second-instance', showWindow);
app.whenReady().then(bootstrap).catch((err: unknown) => {
console.error('[arboretum-desktop] bootstrap failed:', err);
app.quit();
@@ -31,6 +31,20 @@ async function bootstrap(): Promise<void> {
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 {
@@ -73,7 +87,16 @@ function createWindow(url: string): void {
const b = win.getBounds();
saveWindowState({ width: b.width, height: b.height, x: b.x, y: b.y, maximized: win.isMaximized() });
};
win.on('close', persist);
// 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;
});
@@ -81,9 +104,9 @@ function createWindow(url: string): void {
void win.loadURL(`${url}/`);
}
app.on('window-all-closed', () => void shutdown());
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();
@@ -97,6 +120,8 @@ async function shutdown(): Promise<void> {
await daemon?.stop();
} finally {
daemon = null;
tray?.destroy();
tray = null;
app.quit();
}
}