Files
view-mate/TASK-glasses-scroll-fix.md

6.6 KiB
Raw Permalink Blame History

TASK-glasses-scroll-fix.md — фикс скролла диалога на очках + пауза звука телефона

Контекст

После касяков 1–3 (коммит 706cc10) тест на железе выявил два новых дефекта (отчёт 23.08 21:02-21:03):

Дефект А (глаза скроллят по-разному + «всё зависло»): Пользователь: «текст будто бы по-разному скроллится... скролл по-разному работал для левого и правого глаза. мб ошибка связана с тем, что текст рисуется дважды (для каждого глаза)». Корень (установлен расследованием):

  • Binocular() (app-glasses/.../ui/ViewUtils.kt) вызывает content ДВАЖДЫ: левый глаз — PositionedBox(x=0) с BinocularScreenSide=LEFT, правый — PositionedBox(x=parentWidth) с BinocularScreenSide=RIGHT.
  • AssistantOverlayContent (app-glasses/.../ui/AssistantOverlay.kt) внутри этого контента создаёт:
    • rememberScrollState() — два независимых экземпляра (по одному на глаз);
    • LaunchedEffect(chatHistory.size, state.assistantText) { animateScrollTo(maxValue) } — два экземпляра, анимируют каждый СВОЙ scrollState;
    • LaunchedEffect(Unit) { app.answerScrollDelta.collect { ... } } — два коллектора на один общий answerScrollDelta: первый читает дельту и обнуляет аккумулятор, второй в это же время видит 0 или уже обработанную дельту → скролл расходится между глазами, автоскролл и свайпы дерутся → визуальная «каша» и ощущение зависания.

Дефект Б (звук на телефоне после паузы на очках): Пользователь: «я нажал три раза (вызвать ассистента). На очках фильм остановился, а на телефоне ещё какое-то время звук шёл». Корень: startAssistantMode() (app-glasses/.../MainActivity.kt, ~строка 251) вызывает movieController.pauseIfPlaying() (пауза ExoPlayer очков), но не вызывает hostConnection.sendPlaybackPositionNow(). Телефон ставит аудио-плеер на паузу ТОЛЬКО по PlaybackPosition (PhoneApp.onPlaybackPosition → audioSync.sync), а статус шлётся раз в ~5 секунд. В onPause/SCREEN_OFF/display OFF очки такой вызов делают — в startAssistantMode забыли.

Правки (только app-glasses; всё остальное НЕ трогать)

1. AssistantOverlay.kt — один scrollState и один коллектор на оба глаза

Сейчас: AssistantOverlay() → overlayBox(app, state, chatHistory) → Binocular { ... AssistantOverlayContent(state, chatHistory) }, а AssistantOverlayContent сама создаёт scrollState и LaunchedEffect'ы — поэтому они дублируются на каждый глаз.

Требование: поднять состояние и эффекты на уровень ВЫШЕ Binocular (в AssistantOverlay()), чтобы они существовали в единственном экземпляре:

  1. В AssistantOverlay() после проверки overlayVisible:
    • val scrollState = rememberScrollState() — ОДИН экземпляр на оба глаза;
    • ОДИН LaunchedEffect(chatHistory.size, assistantText) — автоскролл к концу: if (hasHistory || assistantText.isNotBlank()) scrollState.animateScrollTo(scrollState.maxValue);
    • ОДИН LaunchedEffect(Unit) — коллектор app.answerScrollDelta: логика как сейчас (scrollOffset, обнуление аккумулятора, animateScrollTo(target, tween(150))).
  2. AssistantOverlayContent сделать чистой отрисовкой: параметры (state, chatHistory, scrollState), ВНУТРИ НЕ создавать ни rememberScrollState, ни LaunchedEffect'ов, ни коллекторов. Только Column + header + список chatHistory (как сейчас: «я: ...» белым / «ассистент: ...» 0xFFB0BEC5, verticalScroll(scrollState)) + fallback на assistantText при пустой истории.
  3. overlayBox — передавать scrollState в AssistantOverlayContent (сигнатуры поправить).

Проверка логики: в Binocular контент вызывается дважды, но scrollState/LaunchedEffect теперь живут вне него — оба глаза рисуют одно и то же состояние, гонки нет.

2. MainActivity.kt — мгновенная отправка позиции при вызове ассистента

В startAssistantMode() после app.movieController.pauseIfPlaying() добавить:

GlassesApp.instance.hostConnection.sendPlaybackPositionNow()

(по образцу onPause/SCREEN_OFF: пауза + немедленный статус, чтобы телефон тут же поставил звук на паузу).

3. Сборка, тесты, коммит

  • ./gradlew :lib-core:build :app-glasses:assembleDebug :app-phone:assembleDebug — BUILD SUCCESSFUL, тесты проходят.
  • git add -A && git commit -m 'Очки: единый скролл диалога на оба глаза + мгновенная пауза звука телефона при вызове ассистента' (БЕЗ push).

КРИТИЧЕСКИЕ ПРАВИЛА

  • workspace = /root/WORK/view-mate. Пути ТОЛЬКО относительные от корня (app-glasses/...). НИКОГДА /root/WORK/app-glasses (без view-mate) — external_directory, permission-guard отклонит.
  • НЕ трогать: app-phone, lib-core, LocalLlm.kt (GPU), скорость, клавиатуру, автоименование, жесты (AssistantGestures.kt), матрицу режимов.
  • Коммит одного файла оверлея + одного метода MainActivity.
  • В конце — краткий отчёт: файлы, что изменено, результат сборки/тестов.