Files
laude/src/main/index.ts
AnRil 4745f5e091 perf+fix: sprint B — async I/O, before-quit, immutable getState, lucide tree-shake
#2  atomicWrite spin-loop → async setTimeout. Раньше при retry на
    EBUSY/EPERM (антивирус, OneDrive) main process замораживался на
    50/200/800ms × до 3 итераций ≈ секунда залипания UI. Сейчас async
    sleep — event-loop живёт. Сохранён atomicWriteSync для flushNow
    (вызывается из before-quit когда event-loop уже умирает).
    Аналогичный фикс в games/steam-launch-options.ts.
#5  before-quit теперь дожидается stopGamesRegistry через
    e.preventDefault() + app.exit(0). Раньше GSI HTTP server не успевал
    closeAllConnections до exit, и следующий запуск получал
    EADDRINUSE на port 4701 (TIME_WAIT) — GSI молча не работал.
#10 IPC.getState возвращает поверхностную копию settings вместо мутации
    кэша. Раньше startWithWindows писалось напрямую в state.settings,
    разъезжаясь с persisted-disk-значением до следующего mutation.
#19 lib/icon.tsx: `import * as Lucide` (wildcard, ~500KB в bundle,
    1500+ иконок) → explicit named imports + ICON_MAP. В bundle
    остаются только 18 ICON_CHOICES.
2026-05-22 01:15:31 +07:00

106 lines
3.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { app, BrowserWindow, nativeTheme, systemPreferences } from 'electron'
import {
createMainWindow,
createReminderWindow,
showMainWindow
} from './windows'
import { registerIpc } from './ipc'
import { startScheduler, stopScheduler } from './scheduler'
import { createTray } from './tray'
import { flushNow, getState } from './store'
import { wasStartedHidden } from './autostart'
import { broadcastState } from './state-actions'
import { startGamesRegistry, stopGamesRegistry } from './games/registry'
import { initUpdater, stopUpdater } from './updater'
import { IPC } from '@shared/ipc'
const APP_ID = 'com.anril.exercise-reminder'
// Must be set BEFORE app.whenReady() for Windows toasts to show
// the correct app name / icon in Action Center.
app.setAppUserModelId(APP_ID)
app.setName('Exercise Reminder')
const gotLock = app.requestSingleInstanceLock()
if (!gotLock) {
app.quit()
} else {
app.on('second-instance', () => showMainWindow())
app.whenReady().then(() => {
registerIpc()
createTray()
const hidden = wasStartedHidden() || getState().settings.startMinimized
createMainWindow(!hidden)
// Pre-create the reminder window so first-trigger is instant (no load lag).
createReminderWindow()
startScheduler()
startGamesRegistry().catch((err) =>
console.error('games registry failed:', err)
)
initUpdater()
nativeTheme.on('updated', () => {
const theme = nativeTheme.shouldUseDarkColors ? 'dark' : 'light'
for (const win of BrowserWindow.getAllWindows()) {
if (!win.isDestroyed()) win.webContents.send(IPC.evtThemeChanged, theme)
}
})
try {
systemPreferences.on('accent-color-changed' as never, () => {
try {
const color = '#' + systemPreferences.getAccentColor()
for (const win of BrowserWindow.getAllWindows()) {
if (!win.isDestroyed())
win.webContents.send(IPC.evtAccentChanged, color)
}
} catch {
// ignore
}
})
} catch {
// older Electron / non-Windows
}
})
app.on('window-all-closed', () => {
// Keep running in tray instead of quitting when all windows closed.
if (!getState().settings.minimizeToTray) {
app.quit()
}
})
// Перехватываем первый before-quit, чтобы дождаться `stopGamesRegistry`
// (закрывает GSI HTTP server со всеми pending connections). Без этого
// следующий запуск получает EADDRINUSE на port 4701 (TIME_WAIT), и
// GSI молча не работает. После cleanup'а — реально quit.
let quitting = false
app.on('before-quit', (e) => {
if (quitting) return
e.preventDefault()
quitting = true
stopScheduler()
stopUpdater()
void (async () => {
try {
await stopGamesRegistry()
} catch (err) {
console.error('[index] stopGamesRegistry threw:', err)
}
flushNow()
app.exit(0)
})()
})
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) createMainWindow(true)
else showMainWindow()
})
// Broadcast state once on ready so any prebuilt windows hydrate.
app.whenReady().then(() => broadcastState())
}