Fix the bar not hiding, OK toggles it, and the guide scrolls (0.35.0)
pauljd/davidson-ci: Davidson CI / quality (push) Failing after 1m45s
pauljd/davidson-ci: Davidson CI / ai-review (push) Failing after 1m21s

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-11-01 23:31:27 +00:00
co-authored by Claude Opus 5.5
parent 502f933c60
commit 021cfa2a64
7 changed files with 53 additions and 6 deletions
+13
View File
@@ -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.
+2 -2
View File
@@ -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 }
@@ -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)
@@ -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<MediaDeliveryException>().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() }
@@ -51,6 +51,16 @@ internal fun guideTitleParts(title: String): Pair<String, String?> {
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<SampleChannel>, 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<SampleChannel>, 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<SampleChannel>, 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<SampleChannel>, selectedNumber: Int, modif
}
}
}
}
Text("▲▼ Channel ◄► Time OK Watch Back Home", color = Muted, fontSize = 11.sp)
}
}
@@ -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()) {
@@ -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))
}
}