From 16d207a1173ac264b53d656e0c0a8120015846b6 Mon Sep 17 00:00:00 2001 From: Hermes Agent Date: Wed, 19 Aug 2026 21:14:24 +0300 Subject: [PATCH] =?UTF-8?q?fix(app-phone):=20watch=20=D0=BF=D1=80=D0=B8=20?= =?UTF-8?q?=D0=BF=D0=B5=D1=80=D0=B5=D0=B7=D0=B0=D0=BF=D1=83=D1=81=D0=BA?= =?UTF-8?q?=D0=B5=20=D0=BE=D1=87=D0=BA=D0=BE=D0=B2=20=E2=80=94=20guard=20?= =?UTF-8?q?=D0=BF=D0=BE=20=D1=80=D0=B5=D0=B0=D0=BB=D1=8C=D0=BD=D0=BE=D0=BC?= =?UTF-8?q?=D1=83=20=D1=81=D0=BE=D1=81=D1=82=D0=BE=D1=8F=D0=BD=D0=B8=D1=8E?= =?UTF-8?q?=20(shouldSkipReWatch)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Штатный сценарий (подтверждён на железе): очки перезапустились при живом телефоне — audioSync.isActive остаётся true от прошлого просмотра и блокировал watch («пропущено: аудио-плеер уже активен») до перезапуска телефона. - shouldSkipReWatch(playing, positionMs) = playing && positionMs > 0: повторный тап пропускается только если очки реально играют; иначе audioSync.stop() и новый watch - Обрыв-сценарий не затронут (позиция>0 → guard пропускает, resume) - Тесты 70/0 (3 новых); железо: перезапуск очков → тап «Смотреть» → просмотр с 0 + звук синхронен (diff 145) --- .../binom/viewmate/phone/ui/DetailsScreen.kt | 27 ++++++++++++++----- .../binom/viewmate/phone/ui/ComponentsTest.kt | 17 ++++++++++++ 2 files changed, 38 insertions(+), 6 deletions(-) diff --git a/app-phone/src/main/kotlin/pw/binom/viewmate/phone/ui/DetailsScreen.kt b/app-phone/src/main/kotlin/pw/binom/viewmate/phone/ui/DetailsScreen.kt index c6f5913..214bd01 100644 --- a/app-phone/src/main/kotlin/pw/binom/viewmate/phone/ui/DetailsScreen.kt +++ b/app-phone/src/main/kotlin/pw/binom/viewmate/phone/ui/DetailsScreen.kt @@ -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() when (item.Type) { diff --git a/app-phone/src/test/kotlin/pw/binom/viewmate/phone/ui/ComponentsTest.kt b/app-phone/src/test/kotlin/pw/binom/viewmate/phone/ui/ComponentsTest.kt index fd6dbcb..00446e2 100644 --- a/app-phone/src/test/kotlin/pw/binom/viewmate/phone/ui/ComponentsTest.kt +++ b/app-phone/src/test/kotlin/pw/binom/viewmate/phone/ui/ComponentsTest.kt @@ -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))