VEU.WathBody/1-docs/4-modules/m12-watch-channel/api_specs/04-ui-integration.md

6.7 KiB
Raw Blame History

Спецификация 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 %)» · «пульс: <bpm|—>» · «сеть: онлайн/офлайн» (сеть — как в 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. Манифест

<uses-permission android:name="android.permission.BLUETOOTH_SCAN"
    android:usesPermissionFlags="neverForLocation" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<uses-permission android:name="android.permission.BLUETOOTH"
    android:maxSdkVersion="30" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN"
    android:maxSdkVersion="30" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />

(BLUETOOTH_CONNECT — без neverForLocation, только SCAN.)

6. Тесты (pure JVM)

  • форматирование статус-строки: все WatchStatus × батарея есть/нет × пульс есть/устарел (тексты из спец 03 §1, заряд опускается при null);
  • зона пульса → слово-зона и цветовой токен (границы 50/70/85 % от Hr.MaxBpm по ZoneCalculator);
  • кнопка «Взять с часов»: active/inactive по freshHr, подстановка значения;
  • логика «Больше не спрашивать» — на уровне VM-флага (системный диалог — ручная приёмка);
  • Compose-иерархия — без unit (ручной смок владельца, критерий 3).