Bug: при отмене save-dialog или open-dialog DataCard показывал тост
«Не удалось сохранить» / «Файл не подошёл». Но cancel — это не ошибка.
Расширил IPC возврат: { ok, canceled, path?, error? }. UI теперь
различает: ok → success toast, !ok && canceled → молча, !ok && !canceled
→ error toast.
+9 тестов на validateSettingsPatch для voicePromptsEnabled,
meetingAutoPause, lastSeenVersion (semver-regex / null-сброс /
malformed). Итого 159 → 168 тестов.
Settings → About теперь показывает текущую версию приложения
(раньше была только кнопка «Что нового»). Загружается через
IPC.getAppVersion при mount.
159 lines
6.3 KiB
TypeScript
159 lines
6.3 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),
|
|
getAppVersion: (): Promise<string> => ipcRenderer.invoke(IPC.getAppVersion),
|
|
getMeetingActive: (): Promise<boolean> =>
|
|
ipcRenderer.invoke(IPC.getMeetingActive),
|
|
|
|
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),
|
|
markChallengeDone: (id: string, reps: number): Promise<boolean> =>
|
|
ipcRenderer.invoke(IPC.markChallengeDone, id, reps),
|
|
|
|
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(IPC.devSimulateMatchEnd, 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),
|
|
|
|
// Export / Import — открывают native save/open dialogs из main process.
|
|
exportState: (): Promise<{
|
|
ok: boolean
|
|
canceled: boolean
|
|
path: string | null
|
|
error?: string
|
|
}> => ipcRenderer.invoke(IPC.exportState),
|
|
importState: (): Promise<{
|
|
ok: boolean
|
|
canceled: boolean
|
|
error?: string
|
|
}> => ipcRenderer.invoke(IPC.importState),
|
|
|
|
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),
|
|
onMeetingChanged: (h: Handler<boolean>): Unsub =>
|
|
on(IPC.evtMeetingChanged, h),
|
|
onHistoryChanged: (h: Handler<void>): Unsub =>
|
|
on(IPC.evtHistoryChanged, h)
|
|
}
|
|
|
|
contextBridge.exposeInMainWorld('api', api)
|
|
|
|
export type Api = typeof api
|