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();
    }
  });
}