Files
view-mate/TASK.md
T

107 lines
6.7 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Задача: масштаб на всех экранах очков + сохранение скролла каталога
## Баг 1: ConnectionScreen игнорирует масштаб
**Симптом (пользователь):** «в режиме НЕ просмотра видео (пока ничего не включено)
очки игнорируют выставленный масштаб». Плюс «масштаб будто бы не сохраняется —
сразу после старта масштаб 1».
**Корень:** масштаб (`GlassesApp.instance.scale`, 0.5..1) применяется ТОЛЬКО в
`GlassesMovieScreen.kt` (контейнер `width((screenWidthPx * scale).toInt().px)` +
offset к центру). А `ConnectionScreen()` в
`app-glasses/src/main/kotlin/pw/binom/viewmate/glasses/MainActivity.kt` (строка ~354)
рисует контент на ПОЛНУЮ половину глаза (`Modifier.width(screenWidthPx.px)`) —
scale не читает вообще. Поэтому и «не сохраняется»: сохранённый scale в prefs есть,
но на не-видео экране он не виден.
**Фикс:** в `ConnectionScreen()` применять scale так же, как в GlassesMovieScreen:
```kotlin
fun ConnectionScreen() {
val connection = GlassesApp.instance.hostConnection
val connected by connection.connected.collectAsState()
val status by connection.status.collectAsState()
val messages by connection.messages.collectAsState()
val scale by GlassesApp.instance.scale.collectAsState() // ← НОВОЕ
Binocular(scale = false) {
val screenWidthPx = BinocularScreenSize.current.width
val screenHeightPx = BinocularScreenSize.current.height
// масштаб через layout — как в GlassesMovieScreen
val containerW = (screenWidthPx * scale).toInt()
val containerH = (screenHeightPx * scale).toInt()
val offsetX = (screenWidthPx * (1f - scale)).toInt() // сдвиг к носу (левый глаз)
Box(
modifier = Modifier
.width(containerW.px)
.height(containerH.px)
.offset(x = offsetX.px)
.background(Color.Black),
) {
... существующий контент ...
}
}
}
```
Внутри контейнера всё остаётся: Column с текстами, StatusPanelCompose,
оверлей распознавания — но всё это теперь в уменьшенном контейнере.
Импорт `androidx.compose.foundation.layout.offset` и `width/height` уже есть.
⚠️ ВАЖНО: `Modifier.offset(x=...)` — смещение внутри родителя. Внутри
`Binocular(scale=false)` контент рисуется на половину 640. При scale<1 контейнер
уже (например 448 при 0.7) и сдвинут к центру (к носу): offsetX = 640*(1-0.7)=192.
Для левого глаза сдвиг вправо (к центру 640), правый глаз — как в GlassesMovieScreen
(offset 0, контейнер прижат к центру слева). Смотри `scaleOffsetX` в
GlassesMovieScreen.kt:143 — скопировать ту же логику (side из BinocularContent,
там есть `Side.LEFT/RIGHT` через CompositionLocal).
## Баг 2: скролл каталога теряется при навигации
**Симптом (пользователь):** «кручу вниз до нужной папки, захожу, тыкаю назад
(кнопку или аппаратную) и я оказываюсь в предыдущем окне в самом начале (скролл
не сохранился). Надо сохранять скролл, даже если зашёл в папку, потом ещё в
папку, потом ещё — и т.д.»
**Корень:** `app-phone/src/main/kotlin/pw/binom/viewmate/phone/ui/CatalogScreen.kt`:
`LazyVerticalGrid` (строка 220) БЕЗ `LazyGridState` — позиция не хранится.
`CatalogState` живёт на корне (переживает переходы), но позицию скролла не помнит.
**Фикс:** добавить в `CatalogState`:
- `private var scrollIndex by mutableStateOf(0)` / `scrollOffset by mutableStateOf(0)` — текущая позиция
- `private val savedScroll = mutableMapOf<Path, Pair<Int, Int>>()` — сохранённые позиции по пути
- в `openSeries`/`openSeason`/`openFolder`: **сначала** `savedScroll[path] = scrollIndex to scrollOffset`, **потом** менять path
- в `back()`: после смены path — восстановить `(scrollIndex, scrollOffset) = savedScroll[path] ?: (0,0)`
- методы `setScroll(index, offset)` (для snapshotFlow из UI) и `getSavedScroll(): Pair<Int,Int>` (текущий path)
В `CatalogScreen` composable:
```kotlin
val gridState = rememberLazyGridState()
// восстанавливаем позицию при смене пути/загрузке
LaunchedEffect(state.path, state.items) {
val (idx, off) = state.savedScrollForCurrentPath()
gridState.scrollToItem(idx, off)
}
// сохраняем позицию при скролле
LaunchedEffect(gridState) {
snapshotFlow {
gridState.firstVisibleItemIndex to gridState.firstVisibleItemScrollOffset
}.collect { (idx, off) -> state.setScroll(idx, off) }
}
```
и `LazyVerticalGrid(state = gridState, ...)`.
Ключ сохранения — РОДИТЕЛЬСКИЙ path (тот, откуда ушли). При back() path меняется
на родителя — достаём сохранённое. Вложенные папки работают автоматически:
каждый уровень сохраняет свою позицию.
Импорты: `androidx.compose.foundation.lazy.grid.rememberLazyGridState`,
`androidx.compose.runtime.snapshotFlow`.
## Критерии приёмки
1. Сборка: `./gradlew :app-phone:assembleDebug :app-glasses:assembleDebug` — успех.
2. Юнит-тесты: `./gradlew :lib-core:testDebugUnitTest :app-phone:testDebugUnitTest :app-glasses:testDebugUnitTest` — зелёные.
3. Ручная проверка на очках: выставить масштаб 0.7 → ConnectionScreen (без видео) тоже уменьшен (чёрные поля по краям, картинка/текст сдвинуты к носу).
4. Ручная проверка: скролл каталога вниз → зайти в папку → назад → позиция на месте; то же для вложенных папок.