/** * Заметки релизов для UI «Что нового». Хардкодные — синхронизируются * с CHANGELOG.md руками при релизе. Не парсим .md в runtime: лишняя * сложность, плюс отделение «коротких заметок для пользователя» от * «развёрнутого технического CHANGELOG'а». * * Формат: версия → язык → массив пунктов (короткий заголовок + опц. деталь). * Не более 5-7 пунктов на версию, иначе пользователь скроллит. */ import type { Language } from './types' export type ReleaseNoteItem = { /** Лаконичная строка-заголовок (≤ 60 символов). */ title: string /** Опциональная одна строка-деталь (≤ 140 символов). */ detail?: string /** Категория для tint'а иконки. */ tag?: 'new' | 'fix' | 'security' | 'perf' } export type ReleaseNotes = Record export const RELEASE_NOTES: Record = { '0.5.8': { ru: [ { title: 'Heatmap и стрик обновляются сразу после нажатия «Готово»', detail: 'Был регресс — статистика не обновлялась пока не изменишь упражнение. Починено через новое событие evtHistoryChanged.', tag: 'fix' }, { title: 'Двойной клик на ✓ больше не пишет 2 раза', detail: 'Rapid double-click на ✓ в Match Summary и «Готово» давал лишние повторы в стрик. Добавлен ref-based дедуп.', tag: 'fix' }, { title: 'Native save/open dialogs локализованы', tag: 'fix' }, { title: '+18 тестов на новые модули', detail: 'achievements, match-challenge edge cases, deleted exercise survival.', tag: 'new' } ], en: [ { title: 'Heatmap and streak update immediately after pressing "Done"', detail: 'There was a regression — stats did not update until you edited an exercise. Fixed via new evtHistoryChanged event.', tag: 'fix' }, { title: 'Double-click on ✓ no longer writes twice', detail: 'Rapid double-click on ✓ in Match Summary and "Done" added extra reps to streak. ref-based dedup.', tag: 'fix' }, { title: 'Native save/open dialogs localised', tag: 'fix' }, { title: '+18 tests for new modules', detail: 'achievements, match-challenge edge cases, deleted exercise survival.', tag: 'new' } ] }, '0.5.7': { ru: [ { title: 'Челленджи из матчей идут в историю', detail: 'Раньше ✓ в Match Summary не считался — стрик и достижения игнорировали игровые тренировки. Исправлено.', tag: 'fix' }, { title: 'Пауза из трея и из Dashboard теперь синхронизированы', detail: 'Раньше Dashboard показывал «running» когда tray был на паузе.', tag: 'fix' }, { title: 'Удаление упражнения спрашивает подтверждение', tag: 'fix' }, { title: 'Daily goal: «Цель закрыта · 100/100»', detail: 'Когда дневная цель достигнута — больше не показываем обратный отсчёт «25ч 13м».', tag: 'fix' }, { title: 'Видно когда мы молчим из-за ВКС', detail: 'Запущен Zoom/Teams — на Dashboard баннер «Не дёргаем — ты на встрече».', tag: 'new' }, { title: 'Celebration анимация на новых достижениях', tag: 'new' }, { title: 'Tracking-badge точнее', detail: 'Live / Setup (закрой Steam) / Off — раньше зелёный показывался даже когда launch option не применён.', tag: 'fix' } ], en: [ { title: 'Match challenges now write to history', detail: 'Previously ✓ in Match Summary did not count — streak and achievements ignored game training. Fixed.', tag: 'fix' }, { title: 'Tray pause and Dashboard pause are now synced', detail: "Previously Dashboard showed 'running' while tray was paused.", tag: 'fix' }, { title: 'Exercise deletion asks for confirmation', tag: 'fix' }, { title: 'Daily goal: "Goal hit · 100/100"', detail: 'When the daily goal is met — no more confusing 25h countdown to tomorrow.', tag: 'fix' }, { title: "Visible when we're quiet because of a meeting", detail: "Zoom/Teams running — Dashboard shows a banner: 'You're in a meeting'.", tag: 'new' }, { title: 'Celebration animation on newly unlocked achievements', tag: 'new' }, { title: 'Tracking badge more accurate', detail: 'Live / Setup (close Steam) / Off — previously green even when launch option was not applied.', tag: 'fix' } ] }, '0.5.6': { ru: [ { title: 'Категории напоминаний', detail: 'Помимо упражнений — гидратация, отдых глазам (20-20-20), осанка.', tag: 'new' }, { title: 'Голосовые подсказки', detail: 'Диктор произносит название упражнения и количество. Включается в настройках.', tag: 'new' }, { title: 'Достижения', detail: 'Milestones по количеству повторений и серий — с прогресс-баром до ближайшей цели.', tag: 'new' }, { title: 'Дневная цель', detail: 'Soft-cap на упражнение: набрал N повторов — реминдер умолкает до завтра.', tag: 'new' }, { title: 'Авто-пауза на ВКС', detail: 'Не дёргает напоминаниями, если запущен Zoom/Teams/Discord/Webex/Slack-huddle.', tag: 'new' }, { title: 'Адаптивный шедулер', detail: 'Учит часы, в которые ты честно делаешь упражнение, и сдвигает fire-ы туда.', tag: 'new' }, { title: 'Экспорт и импорт', detail: 'Резервная копия упражнений, истории и настроек в JSON — для переноса на другую машину.', tag: 'new' }, { title: 'Кнопка «Что нового»', detail: 'Это окно. Открывается автоматически после обновления.', tag: 'new' } ], en: [ { title: 'Reminder categories', detail: 'Beyond exercises — hydration, eye rest (20-20-20), posture.', tag: 'new' }, { title: 'Voice prompts', detail: 'Speaks the exercise name and count. Toggle in Settings.', tag: 'new' }, { title: 'Achievements', detail: 'Milestones by total reps and streaks, with a progress bar to the next one.', tag: 'new' }, { title: 'Daily goal', detail: 'Soft cap per exercise: hit N reps and that reminder goes quiet until tomorrow.', tag: 'new' }, { title: 'Meeting auto-pause', detail: 'No reminders while Zoom/Teams/Discord/Webex/Slack-huddle is running.', tag: 'new' }, { title: 'Adaptive scheduling', detail: 'Learns the hours you reliably do an exercise and shifts fires into those windows.', tag: 'new' }, { title: 'Export & import', detail: 'JSON backup of exercises, history and settings — for moving to another machine.', tag: 'new' }, { title: "What's new screen", detail: 'This screen. Opens automatically after an update.', tag: 'new' } ] }, '0.5.5': { ru: [ { title: 'Sandbox для окон', detail: 'Окна изолированы на уровне OS — даже RCE в рендере не достанет main.', tag: 'security' }, { title: 'Self-hosted шрифты', detail: 'Plus Jakarta Sans + Bricolage Grotesque локально в bundle — работает без интернета.', tag: 'security' }, { title: 'Логи для диагностики', detail: '%APPDATA%/Exercise Reminder/logs/latest.log — отдашь файл при проблеме.', tag: 'new' }, { title: 'UI не залипает при retry-ях I/O', tag: 'perf' }, { title: 'GSI больше не зависает на TIME_WAIT при выходе', tag: 'fix' } ], en: [ { title: 'Window sandbox', detail: 'OS-level isolation — even RCE in the renderer cannot reach main.', tag: 'security' }, { title: 'Self-hosted fonts', detail: 'Plus Jakarta Sans + Bricolage Grotesque shipped in-bundle — works offline.', tag: 'security' }, { title: 'Diagnostic logs', detail: '%APPDATA%/Exercise Reminder/logs/latest.log — share it when something breaks.', tag: 'new' }, { title: 'UI no longer freezes during I/O retries', tag: 'perf' }, { title: 'GSI port no longer stuck in TIME_WAIT after exit', tag: 'fix' } ] }, '0.5.4': { ru: [ { title: 'Фоновое скачивание апдейта', detail: 'Можно уйти на Dashboard и заниматься — апдейт качается в фоне.', tag: 'new' }, { title: 'Моментальный рестарт', detail: 'Кнопка «Рестарт» — ~1-2 сек до открытия новой версии, без диалогов NSIS.', tag: 'new' } ], en: [ { title: 'Background update download', detail: 'You can go to Dashboard and work — the update keeps downloading.', tag: 'new' }, { title: 'Instant restart', detail: 'Restart button — ~1-2 sec to the new version, no NSIS dialogs.', tag: 'new' } ] } } /** * Возвращает версии, отсортированные desc, для которых есть заметки, * с версиями, не виденными пользователем. Используется для «show whatsnew * after update»: если пользователь прыгнул через несколько версий, показываем * все пропущенные одним списком. */ export function unseenVersions( current: string, lastSeen: string | undefined ): string[] { const all = Object.keys(RELEASE_NOTES).sort(compareSemverDesc) if (!lastSeen) { // Первый запуск этого механизма — показываем только текущую версию // чтобы не перегружать историей. Старые версии показывает только // явный «What's new» из Settings. return all.filter((v) => v === current) } return all.filter((v) => compareSemver(v, lastSeen) > 0 && compareSemver(v, current) <= 0) } function parseSemver(v: string): [number, number, number] { const parts = v.split('.').map((n) => parseInt(n, 10)) return [parts[0] ?? 0, parts[1] ?? 0, parts[2] ?? 0] } function compareSemver(a: string, b: string): number { const [a1, a2, a3] = parseSemver(a) const [b1, b2, b3] = parseSemver(b) if (a1 !== b1) return a1 - b1 if (a2 !== b2) return a2 - b2 return a3 - b3 } function compareSemverDesc(a: string, b: string): number { return -compareSemver(a, b) }