Spaces:
Runtime error
Runtime error
File size: 6,430 Bytes
077865a | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 | 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();
}
});
}
|