602 lines
21 KiB
TypeScript
602 lines
21 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.7.1': {
|
||
ru: [
|
||
{
|
||
title: 'Вернули последний удачный дизайн',
|
||
detail:
|
||
'Редизайн 0.7.0 откатан: интерфейс снова выглядит как сохраненная версия “последнее-удачное”.',
|
||
tag: 'fix'
|
||
},
|
||
{
|
||
title: 'Откат придет через автообновление',
|
||
detail:
|
||
'Версия 0.7.1 опубликована как обычный релиз, чтобы установленная 0.7.0 сама вернулась к старому виду.',
|
||
tag: 'fix'
|
||
}
|
||
],
|
||
en: [
|
||
{
|
||
title: 'Restored the last good design',
|
||
detail:
|
||
'The 0.7.0 redesign was rolled back: the UI is back to the saved last-good version.',
|
||
tag: 'fix'
|
||
},
|
||
{
|
||
title: 'Rollback ships through auto-update',
|
||
detail:
|
||
'Version 0.7.1 is a normal release so installed 0.7.0 copies can return to the old look automatically.',
|
||
tag: 'fix'
|
||
}
|
||
]
|
||
},
|
||
'0.6.6': {
|
||
ru: [
|
||
{
|
||
title: 'Настройки стали панелью состояния',
|
||
detail:
|
||
'Сверху видно, работают ли напоминания, включены ли тихие часы, встречи и запуск вместе с Windows.',
|
||
tag: 'new'
|
||
},
|
||
{
|
||
title: 'Главный экран стал точнее по текстам',
|
||
detail:
|
||
'Дата больше не кричит заглавными буквами, а цели показывают понятные единицы: “осталось 30 раз”.',
|
||
tag: 'fix'
|
||
},
|
||
{
|
||
title: 'Сводные карточки читаются лучше',
|
||
detail:
|
||
'Длинные значения в карточках больше не обрезаются, а сетка спокойнее держит десктопные ширины.',
|
||
tag: 'fix'
|
||
},
|
||
{
|
||
title: 'Безопасный стенд для проверки интерфейса',
|
||
detail:
|
||
'Добавлен dev:renderer: можно открыть UI в браузере с демо-данными и не трогать реальные настройки.',
|
||
tag: 'new'
|
||
}
|
||
],
|
||
en: [
|
||
{
|
||
title: 'Settings now show app status first',
|
||
detail:
|
||
'The top panel shows whether reminders, quiet hours, meeting pause and Windows autostart are active.',
|
||
tag: 'new'
|
||
},
|
||
{
|
||
title: 'Overview copy is clearer',
|
||
detail:
|
||
'The date no longer gets artificial capitalization, and goals show units like “30 reps left”.',
|
||
tag: 'fix'
|
||
},
|
||
{
|
||
title: 'Summary cards are easier to read',
|
||
detail:
|
||
'Long values no longer get clipped, and the card grid behaves better on desktop widths.',
|
||
tag: 'fix'
|
||
},
|
||
{
|
||
title: 'Safe renderer preview for UI checks',
|
||
detail:
|
||
'Added dev:renderer so the UI can be opened with demo data without touching real settings.',
|
||
tag: 'new'
|
||
}
|
||
]
|
||
},
|
||
'0.6.5': {
|
||
ru: [
|
||
{
|
||
title: 'Главный экран стал обзором действий',
|
||
detail:
|
||
'Верхний заголовок теперь показывает состояние: что сделать сейчас, что ждёт, пауза или встреча.',
|
||
tag: 'new'
|
||
},
|
||
{
|
||
title: 'Исправлен запуск с Windows',
|
||
detail:
|
||
'Проверка автозапуска теперь использует тот же путь и аргументы, что и запись в Windows.',
|
||
tag: 'fix'
|
||
},
|
||
{
|
||
title: 'Discord больше не ставит перерывы на паузу',
|
||
detail:
|
||
'Авто-пауза встреч реагирует на Zoom, Teams, Webex и Slack-huddle, но не на обычный Discord.',
|
||
tag: 'fix'
|
||
}
|
||
],
|
||
en: [
|
||
{
|
||
title: 'The main screen is now an action overview',
|
||
detail:
|
||
'The header shows the current state: what to do now, what is due, pause or meeting.',
|
||
tag: 'new'
|
||
},
|
||
{
|
||
title: 'Fixed Start with Windows',
|
||
detail:
|
||
'Autostart now reads Windows login items with the same path and arguments it writes.',
|
||
tag: 'fix'
|
||
},
|
||
{
|
||
title: 'Discord no longer pauses breaks',
|
||
detail:
|
||
'Meeting auto-pause still handles Zoom, Teams, Webex and Slack-huddle, but ignores Discord.',
|
||
tag: 'fix'
|
||
}
|
||
]
|
||
},
|
||
'0.6.4': {
|
||
ru: [
|
||
{
|
||
title: 'Новый видимый бренд “Разомнись”',
|
||
detail:
|
||
'Название стало короче и понятнее для русской аудитории: действие видно сразу.',
|
||
tag: 'new'
|
||
},
|
||
{
|
||
title: 'Сводки на каждом экране',
|
||
detail:
|
||
'Упражнения, питание, игры, челленджи и настройки теперь быстрее сканируются сверху.',
|
||
tag: 'new'
|
||
},
|
||
{
|
||
title: 'Улучшена структура вторичных страниц',
|
||
detail:
|
||
'Важные статусы вынесены выше списков, чтобы сразу видеть активность и проблемные места.',
|
||
tag: 'new'
|
||
}
|
||
],
|
||
en: [
|
||
{
|
||
title: 'New visible “Razomnis” brand',
|
||
detail:
|
||
'The name is shorter and more action-focused for the Russian audience.',
|
||
tag: 'new'
|
||
},
|
||
{
|
||
title: 'Overview cards on every screen',
|
||
detail:
|
||
'Exercises, meals, games, challenges and settings are easier to scan from the top.',
|
||
tag: 'new'
|
||
},
|
||
{
|
||
title: 'Secondary pages are more structured',
|
||
detail:
|
||
'Important statuses moved above long lists so active and risky areas are visible immediately.',
|
||
tag: 'new'
|
||
}
|
||
]
|
||
},
|
||
'0.6.3': {
|
||
ru: [
|
||
{
|
||
title: 'Новый главный экран “Не Залипай”',
|
||
detail:
|
||
'Сегодня теперь показывает не только план, но и недельный ритм, уровень и игровые долги.',
|
||
tag: 'new'
|
||
},
|
||
{
|
||
title: 'Мини-челленджи недели',
|
||
detail:
|
||
'5 дней без нуля, 1000 повторов, “сегодня не ноль” и закрытые катки считаются автоматически.',
|
||
tag: 'new'
|
||
},
|
||
{
|
||
title: 'Игровой долг после каток',
|
||
detail:
|
||
'На Dashboard видно, сколько Dota-долгов закрыто сегодня и за неделю.',
|
||
tag: 'new'
|
||
},
|
||
{
|
||
title: 'Мягкая система уровней',
|
||
detail:
|
||
'XP считается из повторов, активных дней и закрытых игровых челленджей.',
|
||
tag: 'new'
|
||
}
|
||
],
|
||
en: [
|
||
{
|
||
title: 'New “Ne Zalipay” Today screen',
|
||
detail:
|
||
'Today now shows the day plan, weekly rhythm, level and game debts.',
|
||
tag: 'new'
|
||
},
|
||
{
|
||
title: 'Weekly mini-challenges',
|
||
detail:
|
||
'5 non-zero days, 1000 reps, “today is not zero” and closed matches are tracked automatically.',
|
||
tag: 'new'
|
||
},
|
||
{
|
||
title: 'Game debt after matches',
|
||
detail:
|
||
'Dashboard shows how many Dota debts were closed today and this week.',
|
||
tag: 'new'
|
||
},
|
||
{
|
||
title: 'Soft level system',
|
||
detail:
|
||
'XP comes from reps, active days and completed game challenges.',
|
||
tag: 'new'
|
||
}
|
||
]
|
||
},
|
||
'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/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:
|
||
'Pauses reminders while Zoom/Teams/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)
|
||
}
|