feat(app-phone): трекбар-таймлайн в пульте (seek на любую позицию)

- MediaCommand.SEEK (абсолютный seek на seekMs, поле в протоколе уже было)
- MovieController (очки): SEEK → player.seekTo(seekMs.clamp(0,duration)); чистая seekTargetMs
- GlassesScreen (пульт): material3 Slider под статус-карточкой (виден при durationMs>0),
  локальный стейт драга — время под слайдером следует за пальцем (sliderValue ?: positionMs),
  после отпускания цель держится до подтверждения позиции от очков (±2.5с, таймаут 10с),
  звук: SEEK → audio.seekTo(seekMs)
- Тесты 70/0 phone, 37/0 glasses (MovieControllerSeekTest, сериализация SEEK)
This commit is contained in:
Hermes Agent
2026-08-19 21:35:20 +03:00
parent 64f0efde37
commit 8072d0c95a
5 changed files with 112 additions and 1 deletions
@@ -13,6 +13,14 @@ import pw.binom.viewmate.core.MediaCommand
import pw.binom.viewmate.core.protocol.PlayVideo
import java.io.File
/**
* Абсолютная цель seek-а: [seekMs] зажимается в [0, durationMs].
* durationMs ≤ 0 (ещё не известно / нет контента) → зажимаем только снизу.
* Чистая функция — покрыта unit-тестами без Android.
*/
internal fun seekTargetMs(seekMs: Long, durationMs: Long): Long =
seekMs.coerceIn(0L, durationMs.coerceAtLeast(0L))
/**
* Держатель плеера фильма: создаёт/освобождает SimpleExoPlayer,
* выполняет команды из WS-протокола. Сам плеер создаётся только по PlayVideo.
@@ -81,6 +89,11 @@ class MovieController(private val context: Context) {
player.seekTo(target)
}
MediaCommand.SEEK -> {
val target = seekTargetMs(seekMs, player.duration)
player.seekTo(target)
}
// Звук — на телефоне, переключение аудио-дорожек очкам не нужно.
MediaCommand.NEXT_AUDIO, MediaCommand.PREV_AUDIO -> {
log("glasses", "MediaCommand $command игнорируется (звук на телефоне)")