# Спецификация 04: UI — «Тренировка», «Допуск», «Настройки» (интеграция m10) Источники: Ф-3.1–3.3, Ф-13.2, ui-ux разд. 2, module_reqs разд. 2, решения 3/5/6. Единицы 4–7. Все тексты дублируют цвет (🛡). ## 1. Экран «Тренировка» (замена заглушек WorkoutScreen:380/258) ### Статусная строка (Ф-3.1) - слева направо: «часы: <статус> (заряд N %)» · «пульс: » · «сеть: онлайн/офлайн» (сеть — как в m10); - тексты и цвета — по таблице спец 03 §1; зелёный/красный/серый — только дублирование текстом; - заряд опускается (с прочерком) при `battery == null` — формулировка «часы: связь есть», без «заряд —» (решение 3). ### Блок пульса (Ф-3.2) - текущее значение — крупно (≥40 sp) в существующей зоне статуса, окраска по зонам Танака (`ZoneCalculator` m10, `Hr.MaxBpm`; границы — те же, что на «Профиле»): зона 1 recovery <50 % синий, зона 2 лёгкая 50–70 % зелёный, зона 3 средняя 70–85 % жёлтый, зона 4 высокая >85 % красный — рядом с числом название зоны словами, как на «Профиле» m10 («recovery»/«лёгкая»/«средняя»/ «высокая») 🛡; - под значением — линия последнего подхода: Canvas по `store.series(lastSetAtMs, now)`, обновление ~1 Гц, без сеток; пусто/устарело — прочерк «—» вместо линии; - `lastSetAtMs` — время последнего подхода текущей тренировки (VM из `setsFlow`, при отсутствии подходов — старт тренировки); - нет связи/не выбраны/нет разрешения — значение «—», блок сворачивается в статус-строку; голос и ручной ввод не зависят (Ф-1.1, Ф-3.2). ### Финиш - «Завершить» → `finishWorkout(id, avgHr = channel.avgHr(now))`; после записи `store.clear()`; при null среднего — как в m09 (`avg_hr` не отправляется). ## 2. Экран «Допуск» (Ф-13.2) - под полем «Пульс сидя» — вторичная кнопка «Взять с часов»: активна при `freshHr(now) != null` (свежий сэмпл ≤30 с), нажатие подставляет значение в поле (пикером остаётся правка); нет сэмпла — кнопка неактивна с подписью причины («часы не связаны» / «нет свежего значения»); - экран держит канал открытым (`bind(null)`), для допуска достаточно пульса покоя — атлет включает трансляцию заранее (инструкция в «Настройках»). ## 3. Экран «Настройки» — блок «Часы» По образцу блока «Распознавание речи» (m11): - строка статуса канала (текст из спец 03 §1); - «Найти часы» — скан 15 с по UUID 0x180D (запрашивает разрешения при необходимости), список найденных: имя + адрес, тап — выбор (запись `Watch.HrDeviceAddress`, индикатор «выбрано»); - «Забыть часы» — сброс адреса (`""` → NOT_SELECTED); - инструкция включения трансляции — платформо-нейтральный текст: «Включите на часах трансляцию пульса по Bluetooth» + расширяемый справочник производителей (v1 — один пункт: Garmin: «виджет пульса → удержание MENU → Heart Rate Options → Broadcast During Activity = Вкл»); справочник — `map` в коде UI (не БД), добавление производителя — правкой одного списка; - никаких маркателей конкретных моделей в логике. ## 4. Разрешения (runtime) - экраны «Тренировка»/«Допуск»/«Настройки»: при статусе NO_PERMISSION — строка «Нет разрешения Bluetooth» + кнопка «Разрешить» → системный диалог (12+: `BLUETOOTH_SCAN`+`BLUETOOTH_CONNECT` вместе; 8–11: `ACCESS_FINE_LOCATION` с пояснением «для поиска часов»); «Больше не спрашивать» — кнопка ведёт в системные настройки приложения (проверка после возврата); - отказ не блокирует экраны (🛡 инвариант 2): статус-строка показывает причину, остальное работает. ## 5. Манифест ```xml ``` (`BLUETOOTH_CONNECT` — без `neverForLocation`, только SCAN.) ## 6. Тесты (pure JVM) - форматирование статус-строки: все WatchStatus × батарея есть/нет × пульс есть/устарел (тексты из спец 03 §1, заряд опускается при null); - зона пульса → слово-зона и цветовой токен (границы 50/70/85 % от Hr.MaxBpm по ZoneCalculator); - кнопка «Взять с часов»: active/inactive по freshHr, подстановка значения; - логика «Больше не спрашивать» — на уровне VM-флага (системный диалог — ручная приёмка); - Compose-иерархия — без unit (ручной смок владельца, критерий 3).