diff --git a/VERIFICATION-0.76.2.md b/VERIFICATION-0.76.2.md new file mode 100644 index 0000000..f58b551 --- /dev/null +++ b/VERIFICATION-0.76.2.md @@ -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. diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index 525350c..4dd9c50 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 = 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 } diff --git a/android/app/src/main/java/tv/orbit/classic/DriveIn.kt b/android/app/src/main/java/tv/orbit/classic/DriveIn.kt index b47ecd2..c8fa47e 100644 --- a/android/app/src/main/java/tv/orbit/classic/DriveIn.kt +++ b/android/app/src/main/java/tv/orbit/classic/DriveIn.kt @@ -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(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 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 4e47606..768a920 100644 --- a/android/app/src/main/java/tv/orbit/classic/EmbyClient.kt +++ b/android/app/src/main/java/tv/orbit/classic/EmbyClient.kt @@ -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) diff --git a/android/app/src/main/java/tv/orbit/classic/EmbyConnectionScreen.kt b/android/app/src/main/java/tv/orbit/classic/EmbyConnectionScreen.kt index bb7bd2a..5a11943 100644 --- a/android/app/src/main/java/tv/orbit/classic/EmbyConnectionScreen.kt +++ b/android/app/src/main/java/tv/orbit/classic/EmbyConnectionScreen.kt @@ -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 } diff --git a/android/app/src/main/java/tv/orbit/classic/MainActivity.kt b/android/app/src/main/java/tv/orbit/classic/MainActivity.kt index 8b26da1..de61a39 100644 --- a/android/app/src/main/java/tv/orbit/classic/MainActivity.kt +++ b/android/app/src/main/java/tv/orbit/classic/MainActivity.kt @@ -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 { null } +internal val LocalDriveInFocus = staticCompositionLocalOf { 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) } } diff --git a/android/app/src/main/java/tv/orbit/classic/OrbitArcade.kt b/android/app/src/main/java/tv/orbit/classic/OrbitArcade.kt index 1af7e30..834ade0 100644 --- a/android/app/src/main/java/tv/orbit/classic/OrbitArcade.kt +++ b/android/app/src/main/java/tv/orbit/classic/OrbitArcade.kt @@ -61,8 +61,8 @@ internal data class SnakeGame(val width: Int, val height: Int, val body: List 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/SnakeGameTest.kt b/android/app/src/test/java/tv/orbit/classic/SnakeGameTest.kt index d4e1b2d..b854c8a 100644 --- a/android/app/src/test/java/tv/orbit/classic/SnakeGameTest.kt +++ b/android/app/src/test/java/tv/orbit/classic/SnakeGameTest.kt @@ -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() { diff --git a/docs/roadmap.md b/docs/roadmap.md index daf156e..2b06e59 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -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