export type Exercise = { id: string name: string reps: number icon: string intervalMinutes: number enabled: boolean nextFireAt: number lastDoneAt?: number } export type NotificationMode = 'toast' | 'modal' | 'both' export type Theme = 'light' | 'dark' | 'system' export type Language = 'ru' | 'en' /** * Hours when reminders are silenced. `from`/`to` are "HH:MM" 24h strings, * `days` are weekday indices 0=Sun..6=Sat. Empty `days` = applies every day. * If `to <= from` the window wraps across midnight (e.g. 22:00 → 07:00). */ export type QuietHours = { enabled: boolean from: string to: string /** Days when the quiet window is active. */ days: number[] } export type Settings = { globalEnabled: boolean notificationMode: NotificationMode soundEnabled: boolean startWithWindows: boolean minimizeToTray: boolean startMinimized: boolean theme: Theme language: Language snoozeMinutes: number quietHours: QuietHours } export type AppState = { exercises: Exercise[] settings: Settings challenges: Challenge[] gamesEnabled: Partial> history?: HistoryEntry[] } export type HistoryAction = 'done' | 'skip' | 'snooze' export type HistoryEntry = { /** ms epoch */ ts: number exerciseId: string action: HistoryAction /** When user did less than planned. Only meaningful for `done`. */ actualReps?: number } export type Tick = { exerciseId: string msUntilFire: number enabled: boolean } export type FireEvent = { exercise: Exercise mode: NotificationMode } export type GameId = 'dota2' export const GAME_STATS: Record = { dota2: [ 'deaths', 'kills', 'assists', 'last_hits', 'denies', 'duration_min' ] as const } export type GameStat = | 'deaths' | 'kills' | 'assists' | 'last_hits' | 'denies' | 'duration_min' export const STAT_LABELS: Record = { deaths: 'смертей', kills: 'убийств', assists: 'ассистов', last_hits: 'ласт-хитов', denies: 'денаев', duration_min: 'минут матча' } export const STAT_LABELS_EN: Record = { deaths: 'deaths', kills: 'kills', assists: 'assists', last_hits: 'last hits', denies: 'denies', duration_min: 'match minutes' } export function statLabel(stat: GameStat, lang: Language): string { return (lang === 'en' ? STAT_LABELS_EN : STAT_LABELS)[stat] } export type Challenge = { id: string name: string gameId: GameId stat: GameStat multiplier: number exerciseName: string icon: string enabled: boolean } export type LaunchOptionStatus = 'applied' | 'queued' | 'no_user' | 'not_needed' export type GameStatus = { id: GameId name: string installed: boolean installPath?: string integrationActive: boolean // cfg installed + listener running launchOption?: string // e.g. "-gamestateintegration" launchOptionStatus: LaunchOptionStatus steamRunning?: boolean // helps the UI explain queued state enabled: boolean } export type ChallengeResult = { challengeId: string name: string icon: string exerciseName: string reps: number statValue: number /** Pre-localised label for backward compat; renderer prefers `stat`. */ statLabel: string /** Stat key; renderer uses this to localise on demand. */ stat?: GameStat } export type MatchSummary = { gameId: GameId gameName: string durationMs: number won?: boolean results: ChallengeResult[] } export const DEFAULT_SETTINGS: Settings = { globalEnabled: true, notificationMode: 'modal', soundEnabled: true, startWithWindows: false, minimizeToTray: true, startMinimized: false, theme: 'light', language: 'ru', snoozeMinutes: 5, quietHours: { enabled: false, from: '22:00', to: '08:00', days: [0, 1, 2, 3, 4, 5, 6] } } /** * Returns true if `now` falls inside the quiet window. Handles wrap-around * windows (e.g. 22:00 → 08:00). Exposed from shared so both main scheduler * and renderer settings UI can use the same logic. */ export function isQuietAt(qh: QuietHours, now: Date): boolean { if (!qh.enabled) return false const dow = now.getDay() // 0..6 if (qh.days.length > 0 && !qh.days.includes(dow)) return false const [fh, fm] = qh.from.split(':').map(Number) const [th, tm] = qh.to.split(':').map(Number) const cur = now.getHours() * 60 + now.getMinutes() const fromMin = fh * 60 + fm const toMin = th * 60 + tm if (fromMin === toMin) return false if (fromMin < toMin) { // Same-day window. return cur >= fromMin && cur < toMin } // Wraps midnight: active if after `from` today OR before `to` today. return cur >= fromMin || cur < toMin } export const SAMPLE_EXERCISES: Omit[] = [ { name: 'Приседания', reps: 10, icon: 'Activity', intervalMinutes: 30, enabled: true }, { name: 'Отжимания', reps: 10, icon: 'Dumbbell', intervalMinutes: 45, enabled: true }, { name: 'Растяжка спины', reps: 1, icon: 'StretchHorizontal', intervalMinutes: 60, enabled: false } ] export type UpdaterStatus = | { kind: 'idle' } | { kind: 'unsupported'; reason: string } | { kind: 'checking' } | { kind: 'not-available'; currentVersion: string } | { kind: 'available'; version: string; releaseDate?: string } | { kind: 'downloading' percent: number transferred: number total: number bytesPerSecond: number } | { kind: 'downloaded'; version: string } | { kind: 'error'; message: string }