diff --git a/CHANGELOG.md b/CHANGELOG.md index 1fd158a..6a2cdd3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,28 @@ ## [Unreleased] +## [0.6.5] — 2026-06-07 + +### Added + +- Главный экран переосмыслен как обзор действий: верхний заголовок теперь + показывает текущее состояние (`пора сделать`, `следующее`, `встреча`, + `пауза`, `план под контролем`) вместо абстрактного “Сегодня”. +- Добавлены тесты для Windows autostart и для того, что Discord не считается + активной встречей. + +### Changed + +- Пункт главной навигации переименован с “Сегодня” на “Обзор”. +- Тексты meeting auto-pause стали нейтральнее: “Встреча активна”, без + формулировки “Не дёргаем — ты на встрече”. +- Discord убран из списка приложений, которые ставят напоминания на паузу. + +### Fixed + +- Исправлена проверка `Запускать с Windows`: чтение login item теперь использует + тот же `path` и `--hidden`, что и запись через `setLoginItemSettings`. + ## [0.6.4] — 2026-06-07 ### Added @@ -132,7 +154,7 @@ clearHistory/import`, Dashboard на него подписан. с success-зелёным цветом, а не запутанный обратный отсчёт до завтра. - **Авто-пауза на ВКС видна в Dashboard.** Раньше fires пропускались молча — пользователь не понимал почему через 12 мин ничего не пришло. - Сейчас info-баннер «Не дёргаем — ты на встрече» с указанием закрыть + Сейчас info-баннер активной встречи с указанием закрыть Zoom/Teams/etc. - **Native `window.confirm()` → iOS-style ConfirmModal** в restore-операции. Раньше всплывал серый системный диалог. @@ -189,7 +211,7 @@ clearHistory/import`, Dashboard на него подписан. Когда total reps за сегодня (с actualReps) ≥ dailyGoal → scheduler переносит fire на завтра. История = source of truth. - **Авто-пауза на ВКС** (#5) — сканирует процессы tasklist'ом раз в - 30с: Zoom/Teams (старый+new)/Discord/Webex/Slack/Skype/Meet/Whereby/ + 30с: Zoom/Teams (старый+new)/Webex/Slack/Skype/Meet/Whereby/ GoToMeeting. Если запущен — fires не выполняются. - **Адаптивный шедулер** (#2) — opt-in флаг в exercise editor. Heuristic-модель строит hour-of-day success rate по 30 дням истории @@ -505,7 +527,8 @@ days=[Mon..Fri]` теперь правильно проверяется день иконки), системный трей, автозапуск с Windows, native-уведомления, NSIS-инсталлятор, auto-update через electron-updater. -[Unreleased]: https://git.xn--90adajar8af4h.xn--p1ai/AnRil/laude/compare/v0.6.4...HEAD +[Unreleased]: https://git.xn--90adajar8af4h.xn--p1ai/AnRil/laude/compare/v0.6.5...HEAD +[0.6.5]: https://git.xn--90adajar8af4h.xn--p1ai/AnRil/laude/compare/v0.6.4...v0.6.5 [0.6.4]: https://git.xn--90adajar8af4h.xn--p1ai/AnRil/laude/compare/v0.6.3...v0.6.4 [0.6.3]: https://git.xn--90adajar8af4h.xn--p1ai/AnRil/laude/compare/v0.6.2...v0.6.3 [0.6.2]: https://git.xn--90adajar8af4h.xn--p1ai/AnRil/laude/compare/v0.5.8...v0.6.2 diff --git a/README.md b/README.md index 622d7f5..5caf9f2 100644 --- a/README.md +++ b/README.md @@ -2,8 +2,8 @@ Windows desktop приложение, которое помогает делать короткие перерывы без потери фокуса: держит план дня, напоминает размяться, ведёт недельные челленджи и превращает Dota 2 статистику после матча в игровые долги. -[![release](https://img.shields.io/badge/release-v0.6.4-orange)](https://git.xn--90adajar8af4h.xn--p1ai/AnRil/laude/releases/latest) -[![tests](https://img.shields.io/badge/tests-241%20passing-green)]() +[![release](https://img.shields.io/badge/release-v0.6.5-orange)](https://git.xn--90adajar8af4h.xn--p1ai/AnRil/laude/releases/latest) +[![tests](https://img.shields.io/badge/tests-245%20passing-green)]() [![platform](https://img.shields.io/badge/platform-Windows%2010%2F11-blue)]() ## Что внутри @@ -11,7 +11,7 @@ Windows desktop приложение, которое помогает делат - **Гибкие напоминания** — любое количество упражнений, интервал от минуты до часов, разные иконки. - **Питание** — отдельная вкладка с приёмами пищи по времени суток (завтрак/обед/ужин/перекусы), выбор дней недели, пресеты быстрого добавления. Напоминания по настенным часам, а не по интервалу. - **История и стрики** — heatmap-календарь активности, ежедневный счётчик, серия дней подряд. -- **Сегодня** — главный экран с планом дня, уровнем, недельными мини-челленджами и игровым долгом. +- **Обзор** — главный экран с ближайшим действием, планом дня, уровнем, недельными мини-челленджами и игровым долгом. - **Тихие часы** — окно времени когда напоминания подавляются (например `22:00 → 08:00`), с выбором дней недели. - **Сделал частично** — степпер `−/+` в окне напоминания: если ты сделал 5 из 10, в историю запишется честное число. - **Игровая интеграция (Dota 2)** — Game State Integration читает статистику матча, после Победа/Поражение показывает экран с «причитающимися» повторениями (например `10 смертей × 3 = 30 приседаний`). @@ -79,15 +79,16 @@ src/main/store.test.ts (12) src/renderer/src/lib/achievements.test.ts (10) src/shared/release-notes.test.ts (9) src/shared/meals.test.ts (8) -src/main/meeting-detect.test.ts (7) +src/main/meeting-detect.test.ts (8) src/shared/quiet-hours.test.ts (7) src/main/adaptive.test.ts (6) src/renderer/src/lib/day-plan.test.ts (6) src/shared/types.test.ts (4) src/renderer/src/lib/icon-choices.test.ts (4) src/renderer/src/lib/momentum.test.ts (3) +src/main/autostart.test.ts (3) ────────────────────────────────────────── - 241 ✓ + 245 ✓ ``` Покрытие: IPC-валидация (упражнения/челленджи/приёмы пищи), persistence (миграции, карантин битого JSON, history cap), scheduler-гейтинг (тихие часы, ВКС-пауза, daily-goal), планирование приёмов пищи по времени суток (DST-safe), детект ВКС, история/стрики (DST), тихие часы (wrap), парсер VDF для Steam-конфигов, достижения, i18n с плюрализацией RU/EN, дефолты shared-типов. diff --git a/src/main/autostart.test.ts b/src/main/autostart.test.ts new file mode 100644 index 0000000..cee941b --- /dev/null +++ b/src/main/autostart.test.ts @@ -0,0 +1,73 @@ +import { beforeEach, afterEach, describe, expect, it, vi } from 'vitest' + +const h = vi.hoisted(() => ({ + app: { + setLoginItemSettings: vi.fn(), + getLoginItemSettings: vi.fn(() => ({ openAtLogin: false })), + wasOpenedAsHidden: false + } +})) + +vi.mock('electron', () => ({ app: h.app })) + +const originalPlatform = process.platform + +function setPlatform(platform: NodeJS.Platform): void { + Object.defineProperty(process, 'platform', { + value: platform, + configurable: true + }) +} + +async function load(): Promise { + vi.resetModules() + return import('./autostart') +} + +beforeEach(() => { + setPlatform('win32') + h.app.setLoginItemSettings.mockClear() + h.app.getLoginItemSettings.mockReset() + h.app.getLoginItemSettings.mockReturnValue({ openAtLogin: false }) +}) + +afterEach(() => { + setPlatform(originalPlatform) +}) + +describe('autostart', () => { + it('writes Windows login item with the hidden startup argument', async () => { + const { setAutostart } = await load() + + setAutostart(true) + + expect(h.app.setLoginItemSettings).toHaveBeenCalledWith({ + openAtLogin: true, + openAsHidden: true, + path: process.execPath, + args: ['--hidden'] + }) + }) + + it('reads Windows login item using the same path and args', async () => { + h.app.getLoginItemSettings.mockReturnValue({ openAtLogin: true }) + const { isAutostartEnabled } = await load() + + expect(isAutostartEnabled()).toBe(true) + expect(h.app.getLoginItemSettings).toHaveBeenCalledWith({ + path: process.execPath, + args: ['--hidden'] + }) + }) + + it('does nothing on non-Windows platforms', async () => { + setPlatform('linux') + const { setAutostart, isAutostartEnabled } = await load() + + setAutostart(true) + + expect(isAutostartEnabled()).toBe(false) + expect(h.app.setLoginItemSettings).not.toHaveBeenCalled() + expect(h.app.getLoginItemSettings).not.toHaveBeenCalled() + }) +}) diff --git a/src/main/autostart.ts b/src/main/autostart.ts index 3031cf8..f8f77c1 100644 --- a/src/main/autostart.ts +++ b/src/main/autostart.ts @@ -1,19 +1,29 @@ import { app } from 'electron' const HIDDEN_FLAG = '--hidden' +type LoginItemOptions = NonNullable< + Parameters[0] +> + +function loginItemOptions(): LoginItemOptions { + return { + path: process.execPath, + args: [HIDDEN_FLAG] + } +} export function setAutostart(enabled: boolean): void { if (process.platform !== 'win32') return app.setLoginItemSettings({ + ...loginItemOptions(), openAtLogin: enabled, - path: process.execPath, - args: [HIDDEN_FLAG] + openAsHidden: true }) } export function isAutostartEnabled(): boolean { if (process.platform !== 'win32') return false - return app.getLoginItemSettings().openAtLogin + return app.getLoginItemSettings(loginItemOptions()).openAtLogin } export function wasStartedHidden(): boolean { diff --git a/src/main/meeting-detect.test.ts b/src/main/meeting-detect.test.ts index 54b896f..1c73863 100644 --- a/src/main/meeting-detect.test.ts +++ b/src/main/meeting-detect.test.ts @@ -84,13 +84,19 @@ describe('isMeetingActive', () => { }) it('кэширует результат в пределах CACHE_MS (exec вызывается один раз)', async () => { - h.execImpl = (_c, _o, cb) => cb(null, { stdout: csv('discord.exe') }) + h.execImpl = (_c, _o, cb) => cb(null, { stdout: csv('zoom.exe') }) const { isMeetingActive } = await load() await isMeetingActive() await isMeetingActive() expect(h.calls).toBe(1) }) + it('не считает Discord встречей', async () => { + h.execImpl = (_c, _o, cb) => cb(null, { stdout: csv('discord.exe') }) + const { isMeetingActive } = await load() + expect(await isMeetingActive()).toBe(false) + }) + it('при падении tasklist возвращает false и логирует warn', async () => { h.execImpl = (_c, _o, cb) => cb(new Error('ETIMEDOUT')) const { isMeetingActive } = await load() diff --git a/src/main/meeting-detect.ts b/src/main/meeting-detect.ts index 591976d..e137571 100644 --- a/src/main/meeting-detect.ts +++ b/src/main/meeting-detect.ts @@ -1,7 +1,7 @@ /** * Эвристическое обнаружение «человек на ВКС» по списку запущенных процессов. * - * Идея: если запущен Zoom/Teams/Discord/Meet/Webex — пользователь скорее + * Идея: если запущен Zoom/Teams/Meet/Webex — пользователь скорее * всего на встрече или собирается зайти. Останавливаем напоминания, чтобы * не прерывать. После «снятия» процессов возобновляем. * @@ -36,7 +36,6 @@ const MEETING_PROCESSES = new Set([ 'zoom.exe', 'teams.exe', 'ms-teams.exe', // новые Teams 2.0 - 'discord.exe', 'webex.exe', 'webexmta.exe', 'meet.exe', // Google Meet desktop (редкость) diff --git a/src/renderer/src/i18n/dict.ts b/src/renderer/src/i18n/dict.ts index 8a9762c..6904c32 100644 --- a/src/renderer/src/i18n/dict.ts +++ b/src/renderer/src/i18n/dict.ts @@ -12,7 +12,7 @@ export type Dict = Record export const ru: Dict = { // Sidebar / nav - 'nav.today': 'Сегодня', + 'nav.today': 'Обзор', 'nav.exercises': 'Упражнения', 'nav.meals': 'Питание', 'nav.games': 'Игры', @@ -60,11 +60,34 @@ export const ru: Dict = { 'btn.retry': 'Повторить', // Dashboard - 'dashboard.kicker': 'Тренировка дня', - 'dashboard.title': 'Сегодня', + 'dashboard.kicker': 'План перерывов', + 'dashboard.title': 'Что важно сейчас', + 'dashboard.header.date': 'План на {date}', + 'dashboard.header.status.paused': 'пауза', + 'dashboard.header.status.meeting': 'встреча', + 'dashboard.header.status.due': 'ждёт действия', + 'dashboard.header.status.running': 'в работе', + 'dashboard.header.status.clear': 'спокойно', + 'dashboard.header.title.paused': 'Напоминания на паузе', + 'dashboard.header.subtitle.paused': + 'Запусти их снова, когда будешь готов вернуться к коротким перерывам.', + 'dashboard.header.title.meeting': 'Встреча активна', + 'dashboard.header.subtitle.meeting': + 'Пауза на встречах включена. Напоминания продолжатся, когда звонок закончится.', + 'dashboard.header.title.due': 'Пора сделать: {name}', + 'dashboard.header.subtitle.due': + '{kind} · {meta}. Это ближайшее действие по плану.', + 'dashboard.header.title.next': 'Следующее: {name}', + 'dashboard.header.subtitle.next': '{kind} · {meta} · {time}', + 'dashboard.header.title.empty': 'Настрой первый перерыв', + 'dashboard.header.subtitle.empty': + 'Добавь упражнение или питание, чтобы приложение собрало понятный план дня.', + 'dashboard.header.title.clear': 'План под контролем', + 'dashboard.header.subtitle.clear': + 'Срочных действий нет. Ниже видно цели, ритм недели и игровые долги.', 'dashboard.stat.active': 'Активных', 'dashboard.stat.active.of': 'из {total}', - 'dashboard.stat.today_done': 'Сегодня', + 'dashboard.stat.today_done': 'Сделано', 'dashboard.stat.today_done.subtitle': 'повторов за день', 'dashboard.stat.streak': 'Стрик', 'dashboard.stat.streak.subtitle': '{n} дн. подряд', @@ -82,11 +105,11 @@ export const ru: Dict = { 'нужно закрыть Steam и снова открыть', 'dashboard.paused.title': 'Напоминания на паузе', 'dashboard.paused.hint': 'Возобнови, чтобы продолжить отсчёт', - 'dashboard.meeting.title': 'Не дёргаем — ты на встрече', + 'dashboard.meeting.title': 'Встреча активна', 'dashboard.meeting.hint': - 'Запущен Zoom / Teams / Discord / Webex / Slack-huddle. Напоминания возобновятся когда закроешь.', - 'dashboard.plan.title': 'План дня', - 'dashboard.plan.subtitle': 'Следующее действие и дневные цели', + 'Запущен Zoom / Teams / Webex / Slack-huddle. Напоминания возобновятся, когда встреча закончится.', + 'dashboard.plan.title': 'Ближайший шаг', + 'dashboard.plan.subtitle': 'Что сделать сейчас, дневные цели и питание', 'dashboard.plan.due_count': '{n} ждёт', 'dashboard.plan.all_caught_up': 'всё спокойно', 'dashboard.plan.next_action': 'Следующее действие', @@ -145,7 +168,7 @@ export const ru: Dict = { 'momentum.quest.week_reps.desc': 'набери тысячу повторов за неделю', 'momentum.quest.match_debt.title': 'Закрыть катки', 'momentum.quest.match_debt.desc': 'закрой 3 игровых долга за неделю', - 'momentum.quest.today_anchor.title': 'Сегодня не ноль', + 'momentum.quest.today_anchor.title': 'День не ноль', 'momentum.quest.today_anchor.desc': 'сделай хотя бы одно действие сегодня', 'momentum.game.kicker': 'После катки', 'momentum.game.title': 'Игровой долг', @@ -348,7 +371,7 @@ export const ru: Dict = { 'Диктор произносит название упражнения и количество — полезно когда фокус на коде.', 'settings.meeting_pause.label': 'Пауза на встречах', 'settings.meeting_pause.hint': - 'Не дёргать, если запущен Zoom / Teams / Discord / Webex / Slack-huddle.', + 'Ставить напоминания на паузу, если запущен Zoom / Teams / Webex / Slack-huddle.', 'settings.snooze.label': '«Отложить» на', 'settings.snooze.hint': 'Сколько минут добавлять при отложении', 'settings.snooze.1': '1 минута', @@ -501,7 +524,7 @@ export const ru: Dict = { export const en: Dict = { // Sidebar / nav - 'nav.today': 'Today', + 'nav.today': 'Overview', 'nav.exercises': 'Exercises', 'nav.meals': 'Meals', 'nav.games': 'Games', @@ -549,11 +572,34 @@ export const en: Dict = { 'btn.retry': 'Retry', // Dashboard - 'dashboard.kicker': 'Daily training', - 'dashboard.title': 'Today', + 'dashboard.kicker': 'Break plan', + 'dashboard.title': 'What matters now', + 'dashboard.header.date': 'Plan for {date}', + 'dashboard.header.status.paused': 'paused', + 'dashboard.header.status.meeting': 'meeting', + 'dashboard.header.status.due': 'action due', + 'dashboard.header.status.running': 'running', + 'dashboard.header.status.clear': 'clear', + 'dashboard.header.title.paused': 'Reminders are paused', + 'dashboard.header.subtitle.paused': + 'Start them again when you are ready to return to short breaks.', + 'dashboard.header.title.meeting': 'Meeting active', + 'dashboard.header.subtitle.meeting': + 'Meeting pause is enabled. Reminders will continue when the call ends.', + 'dashboard.header.title.due': 'Time to do: {name}', + 'dashboard.header.subtitle.due': + '{kind} · {meta}. This is the closest action in the plan.', + 'dashboard.header.title.next': 'Next: {name}', + 'dashboard.header.subtitle.next': '{kind} · {meta} · {time}', + 'dashboard.header.title.empty': 'Set up your first break', + 'dashboard.header.subtitle.empty': + 'Add an exercise or meal so the app can build a clear day plan.', + 'dashboard.header.title.clear': 'Plan under control', + 'dashboard.header.subtitle.clear': + 'No urgent actions. Goals, weekly rhythm and game debts are below.', 'dashboard.stat.active': 'Active', 'dashboard.stat.active.of': 'of {total}', - 'dashboard.stat.today_done': 'Today', + 'dashboard.stat.today_done': 'Done', 'dashboard.stat.today_done.subtitle': 'reps logged', 'dashboard.stat.streak': 'Streak', 'dashboard.stat.streak.subtitle': '{n} days in a row', @@ -569,12 +615,12 @@ export const en: Dict = { 'dashboard.stat.tracking.subtitle_off': 'disabled', 'dashboard.stat.tracking.subtitle_pending': 'close & reopen Steam', 'dashboard.paused.title': 'Reminders paused', - 'dashboard.meeting.title': "You're in a meeting — won't interrupt", + 'dashboard.meeting.title': 'Meeting active', 'dashboard.meeting.hint': - 'Zoom / Teams / Discord / Webex / Slack-huddle is running. Reminders resume when you close it.', + 'Zoom / Teams / Webex / Slack-huddle is running. Reminders resume when the meeting ends.', 'dashboard.paused.hint': 'Resume to continue countdown', - 'dashboard.plan.title': 'Day plan', - 'dashboard.plan.subtitle': 'Next action and daily goals', + 'dashboard.plan.title': 'Closest step', + 'dashboard.plan.subtitle': 'What to do now, daily goals and meals', 'dashboard.plan.due_count': '{n} due', 'dashboard.plan.all_caught_up': 'all clear', 'dashboard.plan.next_action': 'Next action', @@ -633,7 +679,7 @@ export const en: Dict = { 'momentum.quest.week_reps.desc': 'reach one thousand reps this week', 'momentum.quest.match_debt.title': 'Close matches', 'momentum.quest.match_debt.desc': 'close 3 game debts this week', - 'momentum.quest.today_anchor.title': 'Today is not zero', + 'momentum.quest.today_anchor.title': 'Non-zero day', 'momentum.quest.today_anchor.desc': 'complete at least one action today', 'momentum.game.kicker': 'After match', 'momentum.game.title': 'Game debt', @@ -836,7 +882,7 @@ export const en: Dict = { 'Speaks the exercise name and count — useful when your eyes are on the code.', 'settings.meeting_pause.label': 'Pause during meetings', 'settings.meeting_pause.hint': - 'Skip reminders when Zoom / Teams / Discord / Webex / Slack-huddle is running.', + 'Pause reminders when Zoom / Teams / Webex / Slack-huddle is running.', 'settings.snooze.label': '“Snooze” for', 'settings.snooze.hint': 'How many minutes to postpone', 'settings.snooze.1': '1 minute', diff --git a/src/renderer/src/pages/Dashboard.tsx b/src/renderer/src/pages/Dashboard.tsx index c5329d5..2d99bd8 100644 --- a/src/renderer/src/pages/Dashboard.tsx +++ b/src/renderer/src/pages/Dashboard.tsx @@ -187,18 +187,39 @@ export default function Dashboard(): JSX.Element { lang === 'en' ? 'en-US' : 'ru-RU', { weekday: 'long', day: 'numeric', month: 'long' } ) + const header = dashboardHeaderCopy({ + plan, + paused, + meetingPaused, + hasSetup: exercises.length > 0 || meals.length > 0, + lang, + t + }) return (
-
+
- {today} + {t('dashboard.header.date', { date: today })}
-

- {t('dashboard.title')} -

+
+

+ {header.title} +

+ + {header.status} + +
+

+ {header.subtitle} +