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
@@ -0,0 +1,42 @@
package pw.binom.viewmate.glasses
import kotlin.test.Test
import kotlin.test.assertEquals
/**
* Абсолютный SEEK в MovieController: цель зажимается в [0, duration],
* как при командовании через MediaCommand.SEEK (см. seekTargetMs).
* Чистая логика тестируется без Android-плеера.
*/
class MovieControllerSeekTest {
@Test
fun seekMsInsideDurationPassesThrough() {
assertEquals(123_456L, seekTargetMs(seekMs = 123_456, durationMs = 600_000))
}
@Test
fun negativeSeekClampsToZero() {
assertEquals(0L, seekTargetMs(seekMs = -1, durationMs = 600_000))
assertEquals(0L, seekTargetMs(seekMs = -123_456, durationMs = 600_000))
}
@Test
fun seekBeyondDurationClampsToDuration() {
assertEquals(600_000L, seekTargetMs(seekMs = 600_001, durationMs = 600_000))
assertEquals(600_000L, seekTargetMs(seekMs = 9_999_999, durationMs = 600_000))
}
@Test
fun unknownDurationResolvesToZero() {
// duration ≤ 0 (ещё не известно) — coerceIn(0, 0) даёт 0
assertEquals(0L, seekTargetMs(seekMs = -1, durationMs = 0))
assertEquals(0L, seekTargetMs(seekMs = 123_456, durationMs = 0))
}
@Test
fun exactBoundsAreAllowed() {
assertEquals(0L, seekTargetMs(seekMs = 0, durationMs = 600_000))
assertEquals(600_000L, seekTargetMs(seekMs = 600_000, durationMs = 600_000))
}
}