diff --git a/src/main/validate.ts b/src/main/validate.ts index 5b711fe..132bd83 100644 --- a/src/main/validate.ts +++ b/src/main/validate.ts @@ -18,7 +18,8 @@ import type { Settings, Theme, Language, - NotificationMode + NotificationMode, + ReminderCategory } from '@shared/types' const MAX_STR_LEN = 200 @@ -33,6 +34,12 @@ const VALID_STATS: GameStat[] = [ 'denies', 'duration_min' ] +const VALID_CATEGORIES: ReminderCategory[] = [ + 'exercise', + 'hydration', + 'eyes', + 'posture' +] const HHMM_RE = /^\d{1,2}:\d{2}$/ function isObj(v: unknown): v is Record { @@ -84,6 +91,7 @@ export function validateExerciseInput( const intervalMinutes = intInRange(raw.intervalMinutes, 1, 24 * 60) const icon = safeStr(raw.icon, 64) ?? 'Activity' const enabled = bool(raw.enabled) ?? true + const category = oneOf(raw.category, VALID_CATEGORIES) // undefined OK = default if ( name === undefined || reps === undefined || @@ -91,7 +99,15 @@ export function validateExerciseInput( ) { return null } - return { name, reps, intervalMinutes, icon, enabled } + const out: Omit = { + name, + reps, + intervalMinutes, + icon, + enabled + } + if (category !== undefined) out.category = category + return out } export function validateExercisePatch( @@ -124,6 +140,11 @@ export function validateExercisePatch( if (v === undefined) return null out.enabled = v } + if ('category' in raw) { + const v = oneOf(raw.category, VALID_CATEGORIES) + if (v === undefined) return null + out.category = v + } // Allow scheduler-controlled fields to be patched (used by store.markDone // through this same boundary), but range-check them. if ('nextFireAt' in raw) { diff --git a/src/renderer/src/ReminderApp.tsx b/src/renderer/src/ReminderApp.tsx index 99a9508..ca0b3ea 100644 --- a/src/renderer/src/ReminderApp.tsx +++ b/src/renderer/src/ReminderApp.tsx @@ -223,7 +223,7 @@ function ExerciseReminder({
- {t('reminder.kicker')} + {t(`category.${exercise.category ?? 'exercise'}.cta`)}

{exercise.name} diff --git a/src/renderer/src/components/ExerciseEditor.tsx b/src/renderer/src/components/ExerciseEditor.tsx index cac283e..dd0f90f 100644 --- a/src/renderer/src/components/ExerciseEditor.tsx +++ b/src/renderer/src/components/ExerciseEditor.tsx @@ -1,5 +1,6 @@ import { useEffect, useState } from 'react' -import type { Exercise } from '@shared/types' +import type { Exercise, ReminderCategory } from '@shared/types' +import { REMINDER_CATEGORIES } from '@shared/types' import { Modal } from './ui/Modal' import { Button } from './ui/Button' import { ICON_CHOICES, Icon } from '../lib/icon' @@ -11,6 +12,7 @@ type Draft = { icon: string intervalMinutes: number enabled: boolean + category: ReminderCategory } const EMPTY: Draft = { @@ -18,7 +20,8 @@ const EMPTY: Draft = { reps: 10, icon: 'Activity', intervalMinutes: 30, - enabled: true + enabled: true, + category: 'exercise' } type Props = { @@ -44,7 +47,8 @@ export function ExerciseEditor({ reps: exercise.reps, icon: exercise.icon, intervalMinutes: exercise.intervalMinutes, - enabled: exercise.enabled + enabled: exercise.enabled, + category: exercise.category ?? 'exercise' }) } else { setDraft(EMPTY) @@ -101,6 +105,26 @@ export function ExerciseEditor({ /> + +
+ {REMINDER_CATEGORIES.map((c) => ( + + ))} +
+
+
{ if (editing) await window.api.updateExercise(editing.id, draft) else await window.api.addExercise(draft) diff --git a/src/shared/types.ts b/src/shared/types.ts index 1e8b963..8852ade 100644 --- a/src/shared/types.ts +++ b/src/shared/types.ts @@ -1,3 +1,20 @@ +/** + * Категория напоминания. По умолчанию `exercise` — для совместимости со + * старыми state'ами (поле optional). Категория влияет на: + * - tint иконки в карточке (hydration синий, eyes фиолетовый и т.д.) + * - текст в окне напоминания («Время попить» вместо «Время тренировки») + * - подсчёт повторений: для hydration/eyes/posture `reps` обычно = 1 + * (это не «N раз», а просто «сделай»). + */ +export type ReminderCategory = 'exercise' | 'hydration' | 'eyes' | 'posture' + +export const REMINDER_CATEGORIES: ReminderCategory[] = [ + 'exercise', + 'hydration', + 'eyes', + 'posture' +] + export type Exercise = { id: string name: string @@ -7,6 +24,8 @@ export type Exercise = { enabled: boolean nextFireAt: number lastDoneAt?: number + /** Default 'exercise' если undefined — обратная совместимость. */ + category?: ReminderCategory } export type NotificationMode = 'toast' | 'modal' | 'both' @@ -253,21 +272,40 @@ export const SAMPLE_EXERCISES: Omit[] = [ reps: 10, icon: 'Activity', intervalMinutes: 30, - enabled: true + enabled: true, + category: 'exercise' }, { name: 'Отжимания', reps: 10, icon: 'Dumbbell', intervalMinutes: 45, - enabled: true + enabled: true, + category: 'exercise' }, { - name: 'Растяжка спины', + name: 'Стакан воды', reps: 1, - icon: 'StretchHorizontal', + icon: 'GlassWater', intervalMinutes: 60, - enabled: false + enabled: false, + category: 'hydration' + }, + { + name: 'Отдых глазам (20-20-20)', + reps: 1, + icon: 'Eye', + intervalMinutes: 20, + enabled: false, + category: 'eyes' + }, + { + name: 'Проверь осанку', + reps: 1, + icon: 'PersonStanding', + intervalMinutes: 25, + enabled: false, + category: 'posture' } ]