#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.
106 lines
3.2 KiB
TypeScript
106 lines
3.2 KiB
TypeScript
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())
|
||
}
|