From 021cfa2a645aec0a60aa64948c663500157943e2 Mon Sep 17 00:00:00 2001 From: Pauljd Date: Sun, 1 Nov 2026 23:31:27 +0000 Subject: [PATCH] Fix the bar not hiding, OK toggles it, and the guide scrolls (0.35.0) Co-Authored-By: Claude Opus 5.5 (1M context) --- VERIFICATION-0.35.0.md | 13 +++++++++++++ android/app/build.gradle.kts | 4 ++-- .../main/java/tv/orbit/classic/EmbyClient.kt | 2 +- .../tv/orbit/classic/EmbyPlaybackDialog.kt | 5 ++++- .../main/java/tv/orbit/classic/GuideGrid.kt | 18 +++++++++++++++++- .../tv/orbit/classic/SecureMediaDataSource.kt | 2 +- .../java/tv/orbit/classic/GuideGridTest.kt | 15 +++++++++++++++ 7 files changed, 53 insertions(+), 6 deletions(-) create mode 100644 VERIFICATION-0.35.0.md diff --git a/VERIFICATION-0.35.0.md b/VERIFICATION-0.35.0.md new file mode 100644 index 0000000..dc02a0c --- /dev/null +++ b/VERIFICATION-0.35.0.md @@ -0,0 +1,13 @@ +# Orbit TV 0.35.0 verification + +## Changed +- Fixed the programme bar not hiding after 5 seconds. It was a bug from the 0.31 stream handover: a player handed over while already playing never reported that it was playing. +- OK now closes the programme bar if it's open and opens it if it isn't. +- The guide scrolls. It draws only the rows that fit, and moves only when the chosen row would leave the screen, ready for the 25-channel lineup. + +## Checked +- assembleDebug, testDebugUnitTest and lintDebug pass; 140 tests, 0 failures. Tests cover the scrolling position, not the bar or OK. +- APK signature verified with apksigner. + +## Not checked +- Not yet run on the box. The bar and OK need Paul's on-TV check. Scrolling can't be seen until there are more channels than fit. diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index 0e4c6ef..5ad43ae 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -9,8 +9,8 @@ android { applicationId = "tv.orbit.classic" minSdk = 26 targetSdk = 36 - versionCode = 35 - versionName = "0.34.0-back-closes" + versionCode = 36 + versionName = "0.35.0-guide-scroll" } buildFeatures { compose = true } compileOptions { sourceCompatibility = JavaVersion.VERSION_17; targetCompatibility = JavaVersion.VERSION_17 } diff --git a/android/app/src/main/java/tv/orbit/classic/EmbyClient.kt b/android/app/src/main/java/tv/orbit/classic/EmbyClient.kt index 80b8336..0c8759c 100644 --- a/android/app/src/main/java/tv/orbit/classic/EmbyClient.kt +++ b/android/app/src/main/java/tv/orbit/classic/EmbyClient.kt @@ -41,7 +41,7 @@ internal class HttpsEmbyTransport(internal val deviceId: String = UUID.randomUUI connection.connectTimeout = 10_000 connection.readTimeout = request.readTimeoutMs ?: if (request.path == "/Sessions/Logout") 5_000 else 15_000 connection.setRequestProperty("Accept", "application/json") - connection.setRequestProperty("X-Emby-Authorization", "Emby Client=\"Orbit TV\", Device=\"Orbit TV\", DeviceId=\"$deviceId\", Version=\"0.34.0\"") + connection.setRequestProperty("X-Emby-Authorization", "Emby Client=\"Orbit TV\", Device=\"Orbit TV\", DeviceId=\"$deviceId\", Version=\"0.35.0\"") request.token?.let { connection.setRequestProperty("X-Emby-Token", it) } if (request.method == "POST") { val bytes = request.body.orEmpty().toByteArray(Charsets.UTF_8) diff --git a/android/app/src/main/java/tv/orbit/classic/EmbyPlaybackDialog.kt b/android/app/src/main/java/tv/orbit/classic/EmbyPlaybackDialog.kt index 2b6adf2..7e1d0d0 100644 --- a/android/app/src/main/java/tv/orbit/classic/EmbyPlaybackDialog.kt +++ b/android/app/src/main/java/tv/orbit/classic/EmbyPlaybackDialog.kt @@ -106,6 +106,7 @@ internal fun EmbyPlaybackDialog(session: EmbySampleSession, plan: EmbyPlaybackPl // A player handed over from the picture is already on schedule and may never report ready again. var joinedSchedule = runningPlayer != null if (player.playbackState == Player.STATE_READY) ready = true + playing = player.isPlaying val listener = object : Player.Listener { override fun onPlayerError(error: PlaybackException) { val safeReason = generateSequence(error.cause) { it.cause }.filterIsInstance().firstOrNull()?.message @@ -206,7 +207,9 @@ internal fun EmbyPlaybackDialog(session: EmbySampleSession, plan: EmbyPlaybackPl KeyEvent.KEYCODE_CHANNEL_UP, KeyEvent.KEYCODE_CHANNEL_DOWN, KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE, KeyEvent.KEYCODE_MEDIA_PLAY, KeyEvent.KEYCODE_MEDIA_PAUSE, KeyEvent.KEYCODE_MEDIA_REWIND, KeyEvent.KEYCODE_MEDIA_FAST_FORWARD) if (!supported) false else { - if (event.action == KeyEvent.ACTION_UP) { + if (event.action == KeyEvent.ACTION_UP && isOk && scheduled != null) { + if (infoVisible) infoVisible = false else showInfo() + } else if (event.action == KeyEvent.ACTION_UP) { when (code) { KeyEvent.KEYCODE_DPAD_CENTER, KeyEvent.KEYCODE_ENTER -> { if (scheduled == null && infoVisible) { if (player.isPlaying) player.pause() else player.play() } diff --git a/android/app/src/main/java/tv/orbit/classic/GuideGrid.kt b/android/app/src/main/java/tv/orbit/classic/GuideGrid.kt index 7d2d589..0b9e124 100644 --- a/android/app/src/main/java/tv/orbit/classic/GuideGrid.kt +++ b/android/app/src/main/java/tv/orbit/classic/GuideGrid.kt @@ -51,6 +51,16 @@ internal fun guideTitleParts(title: String): Pair { return parts[0] to parts.drop(1).joinToString(" • ").ifBlank { null } } +/** The first row to draw, moving only as far as it takes to keep the chosen row on screen. */ +internal fun guideFirstRow(row: Int, first: Int, visible: Int, count: Int): Int { + val last = (count - visible).coerceAtLeast(0) + return when { + row < first -> row + row >= first + visible -> row - visible + 1 + else -> first + }.coerceIn(0, last) +} + /** Where the now line sits across the window, or null when now isn't in view. */ internal fun guideNowFraction(nowMs: Long, windowStart: Long, windowEnd: Long): Float? = if (nowMs in windowStart until windowEnd) (nowMs - windowStart).toFloat() / (windowEnd - windowStart) else null @@ -68,6 +78,7 @@ internal fun GuideGrid(channels: List, selectedNumber: Int, modif var row by remember { mutableIntStateOf(channels.indexOfFirst { it.number == selectedNumber }.coerceAtLeast(0)) } var cursor by remember { mutableLongStateOf(now) } var slotsAhead by remember { mutableIntStateOf(0) } + var firstRow by remember { mutableIntStateOf(0) } val gridFocus = remember { FocusRequester() } val toolsFocus = remember { FocusRequester() } var gridFocused by remember { mutableStateOf(true) } @@ -113,6 +124,9 @@ internal fun GuideGrid(channels: List, selectedNumber: Int, modif repeat(GUIDE_SLOTS) { Text(clock(windowStart + it * GUIDE_SLOT_MS), Modifier.weight(1f).padding(start = 4.dp), color = Color.White, fontSize = 12.sp, fontWeight = FontWeight.SemiBold) } } } + BoxWithConstraints(Modifier.fillMaxWidth().weight(1f)) { + val visibleRows = ((maxHeight + 3.dp) / 43.dp).toInt().coerceAtLeast(1) + firstRow = guideFirstRow(row, firstRow, visibleRows, channels.size) Column(Modifier.fillMaxWidth().focusRequester(gridFocus).onFocusChanged { gridFocused = it.isFocused }.focusable().onPreviewKeyEvent { event -> val native = event.nativeKeyEvent if (native.action != KeyEvent.ACTION_DOWN) return@onPreviewKeyEvent false @@ -129,7 +143,8 @@ internal fun GuideGrid(channels: List, selectedNumber: Int, modif else -> false } }, verticalArrangement = Arrangement.spacedBy(3.dp)) { - channels.forEachIndexed { index, line -> + channels.subList(firstRow, minOf(firstRow + visibleRows, channels.size)).forEachIndexed { offset, line -> + val index = firstRow + offset Row(Modifier.fillMaxWidth().height(40.dp), horizontalArrangement = Arrangement.spacedBy(0.dp)) { val mark = ChannelBrands.forNumber(line.number) Row(Modifier.width(130.dp).fillMaxHeight().background(Panel).padding(start = 6.dp), verticalAlignment = Alignment.CenterVertically) { @@ -156,6 +171,7 @@ internal fun GuideGrid(channels: List, selectedNumber: Int, modif } } } + } Text("▲▼ Channel ◄► Time OK Watch Back Home", color = Muted, fontSize = 11.sp) } } diff --git a/android/app/src/main/java/tv/orbit/classic/SecureMediaDataSource.kt b/android/app/src/main/java/tv/orbit/classic/SecureMediaDataSource.kt index 1275942..4a9a15d 100644 --- a/android/app/src/main/java/tv/orbit/classic/SecureMediaDataSource.kt +++ b/android/app/src/main/java/tv/orbit/classic/SecureMediaDataSource.kt @@ -33,7 +33,7 @@ internal class SecureMediaDataSource(private val session: EmbySampleSession) : B next.instanceFollowRedirects = false next.connectTimeout = 10_000 next.readTimeout = 20_000 - next.setRequestProperty("User-Agent", "OrbitTV/0.34.0") + next.setRequestProperty("User-Agent", "OrbitTV/0.35.0") next.setRequestProperty("Accept-Encoding", "identity") session.mediaHeaders(address).forEach { (key, value) -> next.setRequestProperty(key, value) } if (dataSpec.position != 0L || dataSpec.length != C.LENGTH_UNSET.toLong()) { diff --git a/android/app/src/test/java/tv/orbit/classic/GuideGridTest.kt b/android/app/src/test/java/tv/orbit/classic/GuideGridTest.kt index 3296231..7932760 100644 --- a/android/app/src/test/java/tv/orbit/classic/GuideGridTest.kt +++ b/android/app/src/test/java/tv/orbit/classic/GuideGridTest.kt @@ -82,4 +82,19 @@ class GuideGridTest { assertEquals(1f, programmeProgress(100, 200, 300), .001f) assertEquals(0f, programmeProgress(100, 100, 100), .001f) } + + @Test + fun theGridScrollsOnlyWhenTheChosenRowLeavesTheScreen() { + assertEquals(0, guideFirstRow(3, 0, 8, 25)) + assertEquals(1, guideFirstRow(8, 0, 8, 25)) + assertEquals(5, guideFirstRow(9, 5, 8, 25)) + assertEquals(4, guideFirstRow(4, 5, 8, 25)) + assertEquals(17, guideFirstRow(24, 10, 8, 25)) + } + + @Test + fun aShortLineupNeverScrolls() { + assertEquals(0, guideFirstRow(2, 0, 8, 3)) + assertEquals(0, guideFirstRow(0, 4, 8, 3)) + } }