Новая модель Meal — напоминание по настенным часам (time HH:MM + дни недели), в отличие от interval-based Exercise. Отдельная вкладка «Питание» с пресетами быстрого добавления (Завтрак/Обед/Ужин/Перекус). - shared: тип Meal, meals в AppState, nextMealOccurrence (DST-safe), SAMPLE_MEALS, MEAL_PRESETS; IPC-каналы meal:* + evtFireMeal - main: валидация (строгая HH:MM-проверка диапазона), store-мутаторы с пересчётом nextFireAt, scheduler.checkDueMeals (гейт только globalEnabled, grace-окно 120с, игнор тихих часов/ВКС), notifications.fireMealReminder, IPC-хендлеры - renderer: вкладка Meals + MealEditor (время/дни/иконка), MealReminder в окне напоминания (Поел/Отложить, TTS), пункт в Sidebar, маршрут, i18n RU/EN, иконки UtensilsCrossed/Soup - persistence: meals additive (без bump схемы — старые state'ы получают []) - +24 теста (203 -> 227): nextMealOccurrence, валидаторы приёмов пищи, scheduler meal-gating (вкл/выкл, grace, игнор тихих часов) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
100 lines
2.6 KiB
TypeScript
100 lines
2.6 KiB
TypeScript
import { Notification, app } from 'electron'
|
|
import type {
|
|
Exercise,
|
|
MatchSummary,
|
|
Meal,
|
|
NotificationMode
|
|
} from '@shared/types'
|
|
import { IPC } from '@shared/ipc'
|
|
import {
|
|
createReminderWindow,
|
|
getReminderWindow,
|
|
showReminderWindow
|
|
} from './windows'
|
|
|
|
export function fireReminder(exercise: Exercise, mode: NotificationMode): void {
|
|
if (mode === 'toast' || mode === 'both') showToast(exercise)
|
|
if (mode === 'modal' || mode === 'both') showModal(exercise)
|
|
}
|
|
|
|
export function fireMealReminder(meal: Meal, mode: NotificationMode): void {
|
|
if (mode === 'toast' || mode === 'both') showMealToast(meal)
|
|
if (mode === 'modal' || mode === 'both') showMealModal(meal)
|
|
}
|
|
|
|
function showMealToast(meal: Meal): void {
|
|
if (!Notification.isSupported()) return
|
|
const n = new Notification({
|
|
title: app.getName(),
|
|
body: meal.name,
|
|
silent: false
|
|
})
|
|
n.on('click', () => showReminderWindow())
|
|
n.show()
|
|
}
|
|
|
|
function showMealModal(meal: Meal): void {
|
|
const win = createReminderWindow()
|
|
const send = (): void => {
|
|
win.webContents.send(IPC.evtFireMeal, meal)
|
|
}
|
|
if (win.webContents.isLoading()) {
|
|
win.webContents.once('did-finish-load', send)
|
|
} else {
|
|
send()
|
|
}
|
|
showReminderWindow()
|
|
}
|
|
|
|
export function fireMatchSummary(summary: MatchSummary): void {
|
|
if (Notification.isSupported()) {
|
|
const totalReps = summary.results.reduce((s, r) => s + r.reps, 0)
|
|
const n = new Notification({
|
|
title: `Матч ${summary.gameName} завершён`,
|
|
body: `Челленджей: ${summary.results.length}, всего повторений: ${totalReps}`,
|
|
silent: false
|
|
})
|
|
n.on('click', () => showReminderWindow())
|
|
n.show()
|
|
}
|
|
const win = createReminderWindow()
|
|
const send = (): void => {
|
|
win.webContents.send(IPC.evtMatchEnd, summary)
|
|
}
|
|
if (win.webContents.isLoading()) {
|
|
win.webContents.once('did-finish-load', send)
|
|
} else {
|
|
send()
|
|
}
|
|
showReminderWindow()
|
|
}
|
|
|
|
function showToast(exercise: Exercise): void {
|
|
if (!Notification.isSupported()) return
|
|
const n = new Notification({
|
|
title: app.getName(),
|
|
body: `${exercise.name} — ${exercise.reps}`,
|
|
silent: false
|
|
})
|
|
n.on('click', () => showReminderWindow())
|
|
n.show()
|
|
}
|
|
|
|
function showModal(exercise: Exercise): void {
|
|
const win = createReminderWindow()
|
|
const send = (): void => {
|
|
win.webContents.send(IPC.evtFire, exercise)
|
|
}
|
|
if (win.webContents.isLoading()) {
|
|
win.webContents.once('did-finish-load', send)
|
|
} else {
|
|
send()
|
|
}
|
|
showReminderWindow()
|
|
}
|
|
|
|
export function notifyReminderClosed(): void {
|
|
const win = getReminderWindow()
|
|
if (win && !win.isDestroyed()) win.hide()
|
|
}
|