374 lines
13 KiB
TypeScript
374 lines
13 KiB
TypeScript
/**
|
||
* Заметки релизов для 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<Language, ReleaseNoteItem[]>
|
||
|
||
export const RELEASE_NOTES: Record<string, ReleaseNotes> = {
|
||
'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)
|
||
}
|