feat(meals): вкладка «Питание» — напоминания о еде по времени суток
Новая модель 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>
This commit is contained in:
@@ -1,9 +1,16 @@
|
||||
import { powerMonitor, BrowserWindow } from 'electron'
|
||||
import { IPC } from '@shared/ipc'
|
||||
import type { Exercise, Tick, HistoryEntry } from '@shared/types'
|
||||
import { isQuietAt } from '@shared/types'
|
||||
import { getExercises, getHistory, getSettings, updateExercise } from './store'
|
||||
import { fireReminder } from './notifications'
|
||||
import { isQuietAt, nextMealOccurrence } from '@shared/types'
|
||||
import {
|
||||
getExercises,
|
||||
getHistory,
|
||||
getMeals,
|
||||
getSettings,
|
||||
updateExercise,
|
||||
updateMeal
|
||||
} from './store'
|
||||
import { fireMealReminder, fireReminder } from './notifications'
|
||||
import { broadcastState } from './state-actions'
|
||||
import { isMeetingActiveSync, refreshMeetingState } from './meeting-detect'
|
||||
import { adjustNextFireAt } from './adaptive'
|
||||
@@ -95,6 +102,39 @@ function checkDueExercises(): void {
|
||||
if (anyFired) broadcastState()
|
||||
}
|
||||
|
||||
/**
|
||||
* Окно «опоздания»: приём пищи, чьё время прошло более чем на это, считаем
|
||||
* пропущенным (ноут спал / приложение было выключено) и тихо переносим на
|
||||
* следующее вхождение БЕЗ срабатывания — чтобы не вывалить пачку
|
||||
* напоминаний разом при включении вечером. Чуть больше CHECK_MS с запасом.
|
||||
*/
|
||||
const MEAL_GRACE_MS = 120_000
|
||||
|
||||
/**
|
||||
* Приёмы пищи — по времени суток. В отличие от упражнений, НЕ подчиняются
|
||||
* тихим часам и ВКС-паузе: пользователь явно задал время. Гейтит только
|
||||
* глобальная пауза (globalEnabled). Срабатывает в пределах grace-окна после
|
||||
* запланированного времени; в любом случае переносит nextFireAt вперёд.
|
||||
*/
|
||||
function checkDueMeals(): void {
|
||||
const settings = getSettings()
|
||||
if (!settings.globalEnabled) return
|
||||
const now = Date.now()
|
||||
let anyChanged = false
|
||||
for (const meal of getMeals()) {
|
||||
if (!meal.enabled) continue
|
||||
if (meal.nextFireAt > now) continue
|
||||
if (now - meal.nextFireAt <= MEAL_GRACE_MS) {
|
||||
fireMealReminder(meal, settings.notificationMode)
|
||||
}
|
||||
updateMeal(meal.id, {
|
||||
nextFireAt: nextMealOccurrence(meal.time, meal.days, now)
|
||||
})
|
||||
anyChanged = true
|
||||
}
|
||||
if (anyChanged) broadcastState()
|
||||
}
|
||||
|
||||
function broadcastTicks(): void {
|
||||
const now = Date.now()
|
||||
const ticks: Tick[] = getExercises().map((e) => ({
|
||||
@@ -113,6 +153,7 @@ function tick(): void {
|
||||
if (now - lastCheckAt >= CHECK_MS) {
|
||||
lastCheckAt = now
|
||||
checkDueExercises()
|
||||
checkDueMeals()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user