Spaces:
Runtime error
Runtime error
| import path from 'node:path'; | |
| import { fileURLToPath } from 'node:url'; | |
| import { app, dialog, ipcMain, clipboard, nativeTheme } from 'electron'; | |
| import { startServer, ensureSessionToken, getUnifiedApiKey } from './server.mjs'; | |
| import { loadConfig, saveConfig } from './config.js'; | |
| import { buildTray } from './tray.js'; | |
| import { openDashboard } from './window.js'; | |
| import { todayStats, hourlyRequests, successRateToday } from './stats.js'; | |
| const __dirname = path.dirname(fileURLToPath(import.meta.url)); | |
| const DEFAULT_PORT = 31415; | |
| // Lean posture: one instance, menu-bar only. GPU stays ON β vibrancy | |
| // (the popover/dashboard glass) needs GPU compositing; with hardware | |
| // acceleration disabled, transparent windows render an opaque white. | |
| app.setName('FreeLLMAPI'); | |
| app.setPath('userData', path.join(app.getPath('appData'), 'FreeLLMAPI')); | |
| if (!app.requestSingleInstanceLock()) { | |
| app.quit(); | |
| } else { | |
| let resolvedPort = DEFAULT_PORT; | |
| let sessionToken = ''; | |
| // The dashboard owns the theme (its navbar toggle); the popover and the | |
| // window vibrancy follow. Last choice persists in config; before the | |
| // dashboard has ever reported, fall back to the system appearance β | |
| // matching the dashboard's own prefers-color-scheme default. | |
| let theme: 'dark' | 'light' = | |
| (process.env.FREEAPI_THEME as 'dark' | 'light' | undefined) // dev-only screenshot override | |
| ?? loadConfig().theme | |
| ?? (nativeTheme.shouldUseDarkColors ? 'dark' : 'light'); | |
| nativeTheme.themeSource = theme; | |
| app.on('second-instance', () => { | |
| if (sessionToken) openDashboard(resolvedPort, sessionToken); | |
| }); | |
| // The app lives in the tray; closing the dashboard window must not quit. | |
| app.on('window-all-closed', () => {}); | |
| // ββ popover IPC ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| ipcMain.handle('freeapi:snapshot', () => { | |
| const s = todayStats(); | |
| return { | |
| port: resolvedPort, | |
| requests: s.requests, | |
| tokens: s.tokens, | |
| lastModel: s.lastModel, | |
| successRate: successRateToday(), | |
| hourly: hourlyRequests(), | |
| loginItem: app.getLoginItemSettings().openAtLogin, | |
| theme, | |
| }; | |
| }); | |
| ipcMain.on('freeapi:theme-changed', async (_e, next: 'dark' | 'light') => { | |
| if (next !== 'dark' && next !== 'light') return; | |
| if (next === theme) return; | |
| theme = next; | |
| saveConfig({ ...loadConfig(), theme }); | |
| // Flips the vibrancy materials (popover glass + dashboard backdrop). | |
| nativeTheme.themeSource = theme; | |
| const { getPopoverWindow } = await import('./popover.js'); | |
| getPopoverWindow()?.webContents.send('freeapi:refresh'); | |
| }); | |
| ipcMain.handle('freeapi:open-dashboard', () => openDashboard(resolvedPort, sessionToken)); | |
| ipcMain.handle('freeapi:copy-base-url', () => clipboard.writeText(`http://127.0.0.1:${resolvedPort}/v1`)); | |
| ipcMain.handle('freeapi:copy-api-key', () => clipboard.writeText(getUnifiedApiKey())); | |
| ipcMain.handle('freeapi:set-login-item', (_e, open: boolean) => app.setLoginItemSettings({ openAtLogin: open })); | |
| ipcMain.handle('freeapi:quit', () => app.quit()); | |
| app.whenReady().then(async () => { | |
| if (process.platform === 'darwin') app.dock?.hide(); | |
| const cfg = loadConfig(); | |
| const dbPath = path.join(app.getPath('userData'), 'freeapi.db'); | |
| // Packaged: client/dist ships in extraResources (Resources/client-dist). | |
| // Dev: use this repo's own client/dist (desktop/ lives in the monorepo; | |
| // FREEAPI_REPO can still point at a different checkout if ever needed). | |
| const repoRoot = process.env.FREEAPI_REPO ?? path.resolve(__dirname, '../..'); | |
| const clientDist = app.isPackaged | |
| ? path.join(process.resourcesPath, 'client-dist') | |
| : path.join(repoRoot, 'client/dist'); | |
| try { | |
| const { port } = await startServer({ | |
| dbPath, | |
| clientDist, | |
| host: '127.0.0.1', | |
| preferredPort: cfg.port ?? DEFAULT_PORT, | |
| }); | |
| resolvedPort = port; | |
| saveConfig({ ...cfg, port }); | |
| sessionToken = ensureSessionToken(); | |
| const tray = buildTray(port, sessionToken); | |
| console.log(`[desktop] FreeLLMAPI running on http://127.0.0.1:${port}`); | |
| // Dev-only UI verification: FREEAPI_SHOT=1 opens the popover and the | |
| // dashboard, captures both to /tmp, and quits. FREEAPI_SHOT=hold opens | |
| // the popover and keeps it pinned (blur ignored) so a real screen | |
| // capture can include the compositor's vibrancy. Never set when packaged. | |
| if (process.env.FREEAPI_SHOT && !app.isPackaged) { | |
| const fs = await import('node:fs'); | |
| const { togglePopover, getPopoverWindow } = await import('./popover.js'); | |
| const { getDashboardWindow } = await import('./window.js'); | |
| const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms)); | |
| await sleep(800); | |
| togglePopover(tray); | |
| if (process.env.FREEAPI_SHOT === 'hold') { | |
| const pop = getPopoverWindow(); | |
| pop?.removeAllListeners('blur'); // stay open unfocused | |
| if (pop) fs.writeFileSync('/tmp/freeapi-popover-bounds.json', JSON.stringify(pop.getBounds())); | |
| // FREEAPI_THEME forces a theme for captures β skip the dashboard | |
| // then, or its theme report would immediately override the override. | |
| if (!process.env.FREEAPI_THEME) { | |
| openDashboard(port, sessionToken); | |
| await sleep(2500); | |
| const dashWin = getDashboardWindow(); | |
| if (dashWin) { | |
| dashWin.show(); | |
| dashWin.focus(); | |
| dashWin.moveTop(); | |
| fs.writeFileSync('/tmp/freeapi-dashboard-bounds.json', JSON.stringify(dashWin.getBounds())); | |
| } | |
| } | |
| return; | |
| } | |
| await sleep(1500); | |
| const pop = await getPopoverWindow()?.webContents.capturePage(); | |
| if (pop) fs.writeFileSync('/tmp/freeapi-popover.png', pop.toPNG()); | |
| openDashboard(port, sessionToken); | |
| await sleep(3000); | |
| const dash = await getDashboardWindow()?.webContents.capturePage(); | |
| if (dash) fs.writeFileSync('/tmp/freeapi-dashboard.png', dash.toPNG()); | |
| app.quit(); | |
| } | |
| } catch (err: any) { | |
| dialog.showErrorBox( | |
| 'FreeLLMAPI failed to start', | |
| err?.message ?? String(err), | |
| ); | |
| app.quit(); | |
| } | |
| }); | |
| } | |