Make the bar Home, Drive-In and Arcade, fix down in the Drive-In, quicken Snake (0.76.2)
pauljd/davidson-ci: Davidson CI / quality (push) Failing after 2m13s
pauljd/davidson-ci: Davidson CI / ai-review (push) Failing after 1m22s

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-11-02 14:42:29 +00:00
co-authored by Claude Opus 5.5
parent aa4337de22
commit 42e7c77f22
10 changed files with 30 additions and 23 deletions
+5
View File
@@ -0,0 +1,5 @@
# Orbit TV 0.76.2: Home tab
From Paul's play. The bar is now Home, Drive-In and Arcade, with the Orbit logo just a logo. All Channels stays on the main menu. Down from the bar returns to the Drive-In's events. Snake's pace now climbs faster with every bite, reaching twice as fast at 200 points and a limit of three times.
Checked: build, lint and all unit tests pass. Not checked until installed: down from the bar on the box.
+2 -2
View File
@@ -9,8 +9,8 @@ android {
applicationId = "tv.orbit.classic"
minSdk = 26
targetSdk = 36
versionCode = 81
versionName = "0.76.1-smoother-snake"
versionCode = 82
versionName = "0.76.2-home-tab"
}
buildFeatures { compose = true }
compileOptions { sourceCompatibility = JavaVersion.VERSION_17; targetCompatibility = JavaVersion.VERSION_17 }
@@ -386,7 +386,8 @@ internal fun DriveInScreen(session: EmbySampleSession, plan: ChannelPlan?, visit
val selected = lineup.firstOrNull { it.kind == chosenKind } ?: soonest
val index = lineup.indexOf(selected)
var ticket by remember { mutableStateOf<DriveInBill?>(null) }
val focus = remember { FocusRequester() }
val ownFocus = remember { FocusRequester() }
val focus = LocalDriveInFocus.current ?: ownFocus
LaunchedEffect(active, ticket) { if (active && ticket == null) focus.requestFocus() }
val scene = Math.floorMod(visit, DRIVE_IN_SCENES)
val nav = LocalNavFocus.current
@@ -66,7 +66,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.76.1\"")
connection.setRequestProperty("X-Emby-Authorization", "Emby Client=\"Orbit TV\", Device=\"Orbit TV\", DeviceId=\"$deviceId\", Version=\"0.76.2\"")
request.token?.let { connection.setRequestProperty("X-Emby-Token", it) }
if (request.method == "POST") {
val bytes = request.body.orEmpty().toByteArray(Charsets.UTF_8)
@@ -54,7 +54,7 @@ private const val RESTORE_QUIET_TRIES = 4
@Composable
internal fun EmbyConnectionScreen(modifier: Modifier = Modifier, visible: Boolean,
mainGuide: Boolean, mainHome: Boolean = false, mainDriveIn: Boolean = false, allChannelsPresses: Int = 0, onChannelAvailable: (Boolean) -> Unit, onRestoreChecked: () -> Unit = {}, onGuide: () -> Unit,
mainGuide: Boolean, mainHome: Boolean = false, mainDriveIn: Boolean = false, onChannelAvailable: (Boolean) -> Unit, onRestoreChecked: () -> Unit = {}, onGuide: () -> Unit,
onHome: () -> Unit = {}, onDriveIn: () -> Unit = {}) {
val appContext = androidx.compose.ui.platform.LocalContext.current.applicationContext
val savedStore = remember { RememberedConnectionStore.get(appContext) }
@@ -279,7 +279,6 @@ internal fun EmbyConnectionScreen(modifier: Modifier = Modifier, visible: Boolea
LaunchedEffect(mainGuide, mainHome) {
if (!mainGuide && !mainHome) { watchingChannel = false; channelClockOffset = 0 }
}
LaunchedEffect(allChannelsPresses) { guideCategory = ChannelCategory.All }
LaunchedEffect(mainDriveIn) {
if (mainDriveIn) { driveInVisits++; if (pendingDriveInWatch) { pendingDriveInWatch = false; watchingDriveIn = true } } else watchingDriveIn = false
}
@@ -14,6 +14,7 @@ import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusProperties
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.graphics.Color
@@ -36,6 +37,7 @@ import java.util.Locale
/** The first place on the top bar, so the top of any screen can step straight up to it. */
internal val LocalNavFocus = staticCompositionLocalOf<FocusRequester?> { null }
internal val LocalDriveInFocus = staticCompositionLocalOf<FocusRequester?> { null }
internal val Barlow = FontFamily(Font(R.font.barlow_regular), Font(R.font.barlow_semibold, FontWeight.SemiBold), Font(R.font.barlow_semibold, FontWeight.Bold))
internal val Navy = Color(0xFF071632)
internal val Panel = Color(0xFF12356B)
@@ -68,10 +70,10 @@ internal fun OrbitButton(label: String, modifier: Modifier = Modifier, selected:
/** One place along the top, always a press away. */
@Composable
private fun NavItem(label: String, selected: Boolean, focus: FocusRequester?, onClick: () -> Unit) {
private fun NavItem(label: String, selected: Boolean, focus: FocusRequester?, below: FocusRequester?, onClick: () -> Unit) {
var focused by remember { mutableStateOf(false) }
val corner = androidx.compose.foundation.shape.RoundedCornerShape(5.dp)
Box((if (focus != null) Modifier.focusRequester(focus) else Modifier).height(32.dp).onFocusChanged { focused = it.isFocused }
Box((if (focus != null) Modifier.focusRequester(focus) else Modifier).focusProperties { if (below != null) down = below }.height(32.dp).onFocusChanged { focused = it.isFocused }
.background(if (focused) Yellow else if (selected) Blue else Navy.copy(alpha = .72f), corner).clickable(onClick = onClick).padding(horizontal = 14.dp), contentAlignment = Alignment.Center) {
Text(label, color = if (focused) Navy else Color.White, fontSize = 14.sp, fontWeight = FontWeight.SemiBold, maxLines = 1)
}
@@ -84,7 +86,6 @@ private fun OrbitApp() {
var realChannelAvailable by remember { mutableStateOf(false) }
var section by remember { mutableStateOf("Home") }
var restoreChecked by remember { mutableStateOf(false) }
var allChannelsPresses by remember { mutableIntStateOf(0) }
var playingGame by remember { mutableStateOf(false) }
// With nothing to watch yet, the only place to be is signing in or choosing channels.
LaunchedEffect(realChannelAvailable, restoreChecked) { if (!realChannelAvailable && restoreChecked) section = "Connect Emby" }
@@ -100,8 +101,9 @@ private fun OrbitApp() {
// Back on the main menu closes the app.
BackHandler(realChannelAvailable && section != "TV Guide" && section != "Home") { section = "Home" }
val navFocus = remember { FocusRequester() }
val driveInFocus = remember { FocusRequester() }
Box(Modifier.fillMaxSize().background(Navy)) {
CompositionLocalProvider(LocalNavFocus provides navFocus) {
CompositionLocalProvider(LocalNavFocus provides navFocus, LocalDriveInFocus provides driveInFocus) {
// The Drive-In and a game in play take the whole screen; the bar sits over the Drive-In's sky.
val fullBleed = section == "Drive-In" || playingGame
Column(if (fullBleed) Modifier.fillMaxSize() else Modifier.fillMaxSize().padding(start = 36.dp, end = 36.dp, top = 56.dp, bottom = 16.dp)) {
@@ -111,23 +113,22 @@ private fun OrbitApp() {
mainGuide = section == "TV Guide" && realChannelAvailable,
mainHome = section == "Home" && realChannelAvailable,
mainDriveIn = section == "Drive-In" && realChannelAvailable,
allChannelsPresses = allChannelsPresses,
onChannelAvailable = { realChannelAvailable = it }, onRestoreChecked = { restoreChecked = true },
onGuide = { section = "TV Guide" }, onHome = { section = "Home" }, onDriveIn = { section = "Drive-In" })
}
if (!playingGame) Row(Modifier.fillMaxWidth().padding(start = 36.dp, end = 36.dp, top = 12.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp)) {
HomeCard(Modifier.width(144.dp).height(36.dp), onClick = { if (realChannelAvailable) section = "Home" }) { focused ->
Image(painterResource(R.drawable.orbit_logo), "Orbit", Modifier.fillMaxSize(), colorFilter = if (focused) ColorFilter.tint(Navy) else null)
}
Image(painterResource(R.drawable.orbit_logo), "Orbit", Modifier.width(144.dp).height(36.dp))
if (realChannelAvailable) {
// The whole-screen Drive-In has nothing for down to find by itself, so it's told.
val below = if (section == "Drive-In") driveInFocus else null
Spacer(Modifier.width(8.dp))
NavItem("All Channels", section == "TV Guide", navFocus) { allChannelsPresses++; section = "TV Guide" }
NavItem("Drive-In", section == "Drive-In", null) { section = "Drive-In" }
NavItem("Arcade", section == "Arcade", null) { section = "Arcade" }
NavItem("Home", section == "Home" || section == "TV Guide", navFocus, below) { section = "Home" }
NavItem("Drive-In", section == "Drive-In", null, below) { section = "Drive-In" }
NavItem("Arcade", section == "Arcade", null, below) { section = "Arcade" }
}
Spacer(Modifier.weight(1f))
Text(SimpleDateFormat("EEE d MMM HH:mm", Locale.UK).format(Date(now)), Modifier.background(if (section == "Drive-In") Navy.copy(alpha = .72f) else Color.Transparent).padding(horizontal = 6.dp, vertical = 3.dp), color = Color.White, fontSize = 17.sp)
if (realChannelAvailable) HomeCard(Modifier.size(38.dp, 32.dp), onClick = { section = "Connect Emby" }) { focused ->
if (realChannelAvailable) HomeCard(Modifier.size(38.dp, 32.dp).focusProperties { if (section == "Drive-In") down = driveInFocus }, onClick = { section = "Connect Emby" }) { focused ->
Image(painterResource(R.drawable.orbit_icon_gear), "Settings", Modifier.fillMaxSize().padding(5.dp), colorFilter = if (focused) ColorFilter.tint(Navy) else null)
}
}
@@ -61,8 +61,8 @@ internal data class SnakeGame(val width: Int, val height: Int, val body: List<Sn
alive = grown.size < width * height)
}
/** Starts at a walking pace and quickens with every bite, up to a limit. */
val tickMs get() = (150L - score / 10 * 3).coerceAtLeast(70L)
/** Starts at a walking pace and quickens noticeably with every bite, up to a limit. */
val tickMs get() = (150L - score / 10 * 5).coerceAtLeast(50L)
companion object {
fun start(width: Int, height: Int, random: java.util.Random): SnakeGame {
@@ -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.76.1")
next.setRequestProperty("User-Agent", "OrbitTV/0.76.2")
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()) {
@@ -53,7 +53,8 @@ class SnakeGameTest {
@Test fun itQuickensUpToALimit() {
assertEquals(150L, game(3 to 5, 2 to 5).tickMs)
assertTrue(game(3 to 5, 2 to 5).copy(score = 100).tickMs < 150L)
assertEquals(70L, game(3 to 5, 2 to 5).copy(score = 10_000).tickMs)
assertEquals(100L, game(3 to 5, 2 to 5).copy(score = 100).tickMs)
assertEquals(50L, game(3 to 5, 2 to 5).copy(score = 10_000).tickMs)
}
@Test fun aNewGameStartsWithFoodOffTheSnake() {
+1 -1
View File
@@ -100,7 +100,7 @@ Paul's ideas, 1 November 2026. Not fixes and not scheduled.
- **Orbit Drive-In name (Paul, 1 November 2026):** decided, replacing OrbitOffice. Event types inside it are Orbit Marquee, Orbit Double Bill, Orbit Triple Bill and The Late Show. The briefs and design/interface still show the old name.
- **Orbit Drive-In (Paul, 2 November 2026):** built in 0.67 with three scenes that take turns: his theatre and drive-in pictures, and the CRT. Marquee nights, Friday double bills, Saturday triple bills and late shows from his films, booked a week ahead. Tickets and in-app reminders came in 0.71, 5 minutes before and at the start.
- **Top nav (Paul, 2 November 2026):** All Channels, Drive-In and Arcade along the top of every screen, in 0.76, each with its own screen, keeping the main menu clear. Snake is the Arcade's first game.
- **Top nav (Paul, 2 November 2026):** Home, Drive-In and Arcade along the top of every screen, from 0.76.2, each with its own screen, keeping the main menu clear. Snake is the Arcade's first game.
- **Orbit Arcade (Paul, 2 November 2026):** the look in design/arcade is approved: logo, honeycomb menu and Bedlam screen. Games are OrbitHive Bedlam, Snake, Block Drop, Brick Break, Pairs and Patience. Small games only, so the app stays small: drawn in code with no game engine, and measured for size when built. Not scheduled.
## Channel numbers