fix(app-phone): watch при перезапуске очков — guard по реальному состоянию (shouldSkipReWatch)

Штатный сценарий (подтверждён на железе): очки перезапустились при живом телефоне — audioSync.isActive остаётся true от прошлого просмотра и блокировал watch («пропущено: аудио-плеер уже активен») до перезапуска телефона.
- shouldSkipReWatch(playing, positionMs) = playing && positionMs > 0: повторный тап пропускается только если очки реально играют; иначе audioSync.stop() и новый watch
- Обрыв-сценарий не затронут (позиция>0 → guard пропускает, resume)
- Тесты 70/0 (3 новых); железо: перезапуск очков → тап «Смотреть» → просмотр с 0 + звук синхронен (diff 145)
This commit is contained in:
Hermes Agent
2026-08-19 21:14:24 +03:00
parent 5efe14aed1
commit 16d207a117
2 changed files with 38 additions and 6 deletions
@@ -130,13 +130,20 @@ fun DetailsScreen(
}
fun onWatch() {
// звук уже играет — повторный тап не должен пересоздавать аудио-плеер.
// state.playing в guard НЕ включаем: он остаётся true после рестарта телефона
// (очки продолжают играть), когда аудио-плеер ещё не создан, и блокировал бы запуск звука.
if (app.audioSync.isActive) {
log("watch", "пропущено: аудио-плеер уже активен (audioSync.isActive=true)")
scope.launch { snackbar.showSnackbar("уже играет") }
return
// Очки реально играют (позиция растёт) — повторный тап не должен
// пересоздавать звук. Если очки перезапустились/на паузе (позиция 0
// или не играют) — старый аудио-плеер мёртв: сбрасываем и запускаем
// новый watch (звук пересоздастся с позиции очков).
val playing = app.state.playing
val positionMs = app.server.hub.positionMs.value ?: 0L
if (shouldSkipReWatch(playing, positionMs)) {
log("watch", "пропущено: аудио-плеер уже активен и очки играют (pos=$positionMs)")
scope.launch { snackbar.showSnackbar("уже играет") }
return
}
log("watch", "очки не играют (playing=$playing, pos=$positionMs) — сбрасываю аудио-плеер")
app.audioSync.stop()
}
if (!glassesFully) {
log("watch", "пропущено: контент не скачан на очки (glassesFully=false)")
@@ -590,6 +597,14 @@ private fun ConversionStatus(
}
}
/**
* Guard повторного тапа «Смотреть на очках» при живом аудио-плеере: пропускаем
* перезапуск звука только если очки реально играют (playing=true и позиция >0).
* Очки на паузе или перезапустились (позиция 0) — старый плеер мёртв, нужно сбросить и создать заново.
*/
internal fun shouldSkipReWatch(playing: Boolean, positionMs: Long): Boolean =
playing && positionMs > 0
private fun detailMeta(item: JellyfinItem): String {
val parts = mutableListOf<String>()
when (item.Type) {
@@ -2,9 +2,26 @@ package pw.binom.viewmate.phone.ui
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertTrue
class ComponentsTest {
@Test
fun shouldSkipReWatchWhenPlayingWithPosition() {
assertTrue(shouldSkipReWatch(playing = true, positionMs = 42_000), "очки играют и позиция >0 — повторный тап пропускаем")
}
@Test
fun shouldNotSkipReWatchWhenNotPlaying() {
assertFalse(shouldSkipReWatch(playing = false, positionMs = 42_000), "очки не играют — сбрасываем плеер и перезапускаем watch")
}
@Test
fun shouldNotSkipReWatchWhenPositionZero() {
assertFalse(shouldSkipReWatch(playing = true, positionMs = 0), "позиция 0 — очки перезапустились/на паузе, старый плеер мёртв")
}
@Test
fun bytesToSizeKilobytes() {
assertEquals("512 КБ", bytesToSize(512 * 1024L))