Make the bar Home, Drive-In and Arcade, fix down in the Drive-In, quicken Snake (0.76.2)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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.
|
||||
@@ -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
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user