Раньше после «Скачать» renderer ждал promise (`ipcRenderer.invoke`),
пока electron-updater не завершит весь download. Если пользователь
закрывал Settings и уходил на Dashboard — скачивание продолжалось,
но кнопка возвращалась в `busy=true` при следующем открытии.
Сама установка через `quitAndInstall()` без параметров поднимала
NSIS-диалог установщика — ~5-10 сек до запуска новой версии.
Что изменилось:
- IPC `updaterDownload` / `updaterInstall` — fire-and-forget через
`ipcMain.on` / `ipcRenderer.send`. Renderer триггерит и забывает,
прогресс приходит через `evtUpdaterStatus`. UI моментально
переключается в kind:'downloading' и не блокируется ожиданием.
- `autoUpdater.quitAndInstall(true, true)`:
- isSilent=true: NSIS работает без UI установщика (~1-2 сек
вместо ~5-10), без чёрного окна на половину экрана.
- isForceRunAfter=true: гарантия что приложение запустится
после установки (иначе пользователь нажал «Рестарт» и остался
без открытого приложения).
- UpdaterCard: убран `busy` для async download — статус сам
переключается через события. Добавлена подсказка «можно закрыть
это окно, скачивание продолжится в фоне». Подкручен subtitle на
downloaded-state: «нажми Рестарт — приложение моментально
откроется в новой версии».
- i18n: новый ключ `updater.downloading.hint` (RU + EN), обновлён
`updater.downloaded.subtitle`.
`autoInstallOnAppQuit = true` уже был включён — если пользователь
не нажал «Рестарт» и просто закрыл приложение, установка
произойдёт при следующем закрытии автоматически.
137 lines
5.5 KiB
TypeScript
137 lines
5.5 KiB
TypeScript
import { contextBridge, ipcRenderer } from 'electron'
|
|
import { IPC } from '@shared/ipc'
|
|
import type {
|
|
AppState,
|
|
Challenge,
|
|
Exercise,
|
|
GameId,
|
|
GameStatus,
|
|
HistoryEntry,
|
|
MatchSummary,
|
|
Settings,
|
|
Tick,
|
|
UpdaterStatus
|
|
} from '@shared/types'
|
|
|
|
type Unsub = () => void
|
|
type Handler<T> = (payload: T) => void
|
|
|
|
function on<T>(channel: string, handler: Handler<T>): Unsub {
|
|
const listener = (_e: Electron.IpcRendererEvent, payload: T): void =>
|
|
handler(payload)
|
|
ipcRenderer.on(channel, listener)
|
|
return () => ipcRenderer.removeListener(channel, listener)
|
|
}
|
|
|
|
const api = {
|
|
getState: (): Promise<AppState> => ipcRenderer.invoke(IPC.getState),
|
|
|
|
addExercise: (
|
|
input: Omit<Exercise, 'id' | 'nextFireAt' | 'lastDoneAt'>
|
|
): Promise<Exercise> => ipcRenderer.invoke(IPC.addExercise, input),
|
|
updateExercise: (id: string, patch: Partial<Exercise>): Promise<Exercise> =>
|
|
ipcRenderer.invoke(IPC.updateExercise, id, patch),
|
|
deleteExercise: (id: string): Promise<boolean> =>
|
|
ipcRenderer.invoke(IPC.deleteExercise, id),
|
|
toggleExercise: (id: string, enabled: boolean): Promise<Exercise> =>
|
|
ipcRenderer.invoke(IPC.toggleExercise, id, enabled),
|
|
markDone: (id: string, actualReps?: number): Promise<Exercise> =>
|
|
ipcRenderer.invoke(IPC.markDone, id, actualReps),
|
|
snooze: (id: string, minutes: number): Promise<Exercise> =>
|
|
ipcRenderer.invoke(IPC.snooze, id, minutes),
|
|
skip: (id: string): Promise<Exercise> => ipcRenderer.invoke(IPC.skip, id),
|
|
|
|
updateSettings: (patch: Partial<Settings>): Promise<Settings> =>
|
|
ipcRenderer.invoke(IPC.updateSettings, patch),
|
|
|
|
getAccentColor: (): Promise<string> => ipcRenderer.invoke(IPC.getAccentColor),
|
|
getOsTheme: (): Promise<'light' | 'dark'> =>
|
|
ipcRenderer.invoke(IPC.getOsTheme),
|
|
|
|
pauseAll: (): Promise<void> => ipcRenderer.invoke(IPC.pauseAll),
|
|
resumeAll: (): Promise<void> => ipcRenderer.invoke(IPC.resumeAll),
|
|
quit: (): Promise<void> => ipcRenderer.invoke(IPC.quit),
|
|
reminderClose: (): Promise<void> => ipcRenderer.invoke(IPC.reminderClose),
|
|
|
|
minimizeMain: (): void => ipcRenderer.send(IPC.minimizeMain),
|
|
toggleMaximizeMain: (): void => ipcRenderer.send(IPC.toggleMaximizeMain),
|
|
isMaximizedMain: (): Promise<boolean> =>
|
|
ipcRenderer.invoke(IPC.isMaximizedMain),
|
|
closeMain: (): void => ipcRenderer.send(IPC.closeMain),
|
|
hideMain: (): void => ipcRenderer.send(IPC.hideMain),
|
|
|
|
// Games
|
|
listGames: (): Promise<GameStatus[]> => ipcRenderer.invoke(IPC.gamesList),
|
|
installGame: (id: GameId): Promise<GameStatus> =>
|
|
ipcRenderer.invoke(IPC.gameInstall, id),
|
|
uninstallGame: (id: GameId): Promise<GameStatus> =>
|
|
ipcRenderer.invoke(IPC.gameUninstall, id),
|
|
toggleGame: (id: GameId, enabled: boolean): Promise<void> =>
|
|
ipcRenderer.invoke(IPC.gameToggle, id, enabled),
|
|
openGameLaunchOptions: (id: GameId): Promise<void> =>
|
|
ipcRenderer.invoke(IPC.gameOpenLaunchOptions, id),
|
|
|
|
// Challenges
|
|
addChallenge: (input: Omit<Challenge, 'id'>): Promise<Challenge> =>
|
|
ipcRenderer.invoke(IPC.addChallenge, input),
|
|
updateChallenge: (
|
|
id: string,
|
|
patch: Partial<Challenge>
|
|
): Promise<Challenge> => ipcRenderer.invoke(IPC.updateChallenge, id, patch),
|
|
deleteChallenge: (id: string): Promise<boolean> =>
|
|
ipcRenderer.invoke(IPC.deleteChallenge, id),
|
|
toggleChallenge: (id: string, enabled: boolean): Promise<Challenge> =>
|
|
ipcRenderer.invoke(IPC.toggleChallenge, id, enabled),
|
|
|
|
closeMatchSummary: (): Promise<void> =>
|
|
ipcRenderer.invoke(IPC.closeMatchSummary),
|
|
|
|
// Dev-only: synthesize a match-end event from the renderer. The channel is
|
|
// not registered in production builds (see src/main/ipc.ts), so this
|
|
// function will reject in shipped binaries even though it's exposed.
|
|
// Gated at the preload level too so the bundler can dead-code-eliminate it.
|
|
...(import.meta.env.MODE !== 'production'
|
|
? {
|
|
simulateMatchEnd: (
|
|
id: GameId,
|
|
stats: Record<string, number>
|
|
): Promise<void> =>
|
|
ipcRenderer.invoke('dev:simulateMatchEnd', id, stats)
|
|
}
|
|
: {}),
|
|
|
|
// Auto-updater
|
|
updaterStatus: (): Promise<UpdaterStatus> =>
|
|
ipcRenderer.invoke(IPC.updaterStatus),
|
|
updaterCheck: (): Promise<UpdaterStatus> =>
|
|
ipcRenderer.invoke(IPC.updaterCheck),
|
|
// Fire-and-forget. Прогресс и завершение прилетают через onUpdaterStatus —
|
|
// renderer не должен `await`'ить, иначе busy-state висит весь download.
|
|
updaterDownload: (): void => ipcRenderer.send(IPC.updaterDownload),
|
|
updaterInstall: (): void => ipcRenderer.send(IPC.updaterInstall),
|
|
|
|
// History
|
|
getHistory: (sinceMs?: number): Promise<HistoryEntry[]> =>
|
|
ipcRenderer.invoke(IPC.getHistory, sinceMs),
|
|
clearHistory: (beforeTs?: number): Promise<number> =>
|
|
ipcRenderer.invoke(IPC.clearHistory, beforeTs),
|
|
|
|
onTick: (h: Handler<Tick[]>): Unsub => on(IPC.evtTick, h),
|
|
onFire: (h: Handler<Exercise>): Unsub => on(IPC.evtFire, h),
|
|
onMatchEnd: (h: Handler<MatchSummary>): Unsub => on(IPC.evtMatchEnd, h),
|
|
onStateChanged: (h: Handler<AppState>): Unsub => on(IPC.evtStateChanged, h),
|
|
onThemeChanged: (h: Handler<'light' | 'dark'>): Unsub =>
|
|
on(IPC.evtThemeChanged, h),
|
|
onAccentChanged: (h: Handler<string>): Unsub => on(IPC.evtAccentChanged, h),
|
|
onGamesChanged: (h: Handler<GameStatus[]>): Unsub =>
|
|
on(IPC.evtGamesChanged, h),
|
|
onUpdaterStatus: (h: Handler<UpdaterStatus>): Unsub =>
|
|
on(IPC.evtUpdaterStatus, h),
|
|
onMaximizeChanged: (h: Handler<boolean>): Unsub =>
|
|
on(IPC.evtMaximizeChanged, h)
|
|
}
|
|
|
|
contextBridge.exposeInMainWorld('api', api)
|
|
|
|
export type Api = typeof api
|