Полная автоматизация релизного цикла. == Auto-update (electron-updater) == - src/main/updater.ts — обёртка над autoUpdater с дискриминированным UpdaterStatus union и broadcast через IPC. autoDownload=false, пользователь сам жмёт «Скачать». allowDowngrade=false. Проверка каждые 6 часов, первая через 5с после старта. - В dev-режиме (app.isPackaged=false) статус сразу становится 'unsupported' с пояснением — никаких exceptions из updater'а. - build.publish в package.json: provider=generic, url указывает на Gitea release assets конкретной версии. - src/main/ipc.ts: 4 новых канала — status/check/download/install. - src/preload: API window.api.updater* + onUpdaterStatus. - src/renderer/src/components/UpdaterCard.tsx: HUD-карточка в Settings с состояниями idle/checking/available/downloading/downloaded/error, прогресс-бар с скоростью в МБ/с. == Тесты (vitest) == - vitest.config.ts с алиасами @shared / @renderer - 23 теста, все зелёные: * format.test.ts — formatCountdown, formatInterval (8 cases) * vdf.test.ts — parseVdf / stringifyVdf / round-trip (11 cases) * types.test.ts — DEFAULT_SETTINGS, SAMPLE_EXERCISES sanity (4) - npm scripts: test (watch), test:run (CI) == CI/CD (Gitea Actions) == - .gitea/workflows/ci.yml — на push/PR: typecheck + тесты + smoke-сборка - .gitea/workflows/release.yml — на тег v*.*.*: сборка NSIS + Gitea release == Локальный релизный скрипт == - scripts/release.ps1 — один скрипт от бампа версии до публикации через Gitea API (params: -Bump patch/minor/major, -Version, -DryRun) - npm run release — обёртка - RELEASING.md — полная инструкция Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
87 lines
2.6 KiB
TypeScript
87 lines
2.6 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()
|
|
}
|
|
})
|
|
|
|
app.on('before-quit', () => {
|
|
stopScheduler()
|
|
stopUpdater()
|
|
void stopGamesRegistry()
|
|
flushNow()
|
|
})
|
|
|
|
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())
|
|
}
|