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
@@ -46,6 +46,7 @@ enum class MediaCommand {
TOGGLE,
SEEK_FORWARD,
SEEK_BACKWARD,
SEEK,
NEXT_AUDIO,
PREV_AUDIO,
CLOSE,
@@ -64,6 +64,17 @@ class HostToGlassesTest {
assertEquals(0, decoded.seekMs)
}
@Test
fun seekCommandRoundTrip() {
val msg: HostToGlasses = MediaCommandMsg(command = MediaCommand.SEEK, seekMs = 5_400_000)
val text = json.encodeToString(HostToGlasses.serializer(), msg)
assertEquals("""{"type":"media_command","command":"SEEK","seekMs":5400000}""", text)
val decoded = json.decodeFromString(HostToGlasses.serializer(), text)
assertIs<MediaCommandMsg>(decoded)
assertEquals(MediaCommand.SEEK, decoded.command)
assertEquals(5_400_000, decoded.seekMs)
}
@Test
fun playVideoRoundTrip() {
val msg: HostToGlasses = PlayVideo(