diff --git a/VERIFICATION-0.30.0.md b/VERIFICATION-0.30.0.md new file mode 100644 index 0000000..885d2d4 --- /dev/null +++ b/VERIFICATION-0.30.0.md @@ -0,0 +1,16 @@ +# Orbit TV 0.30.0 verification + +OrbitOffice is now Orbit Drive-In, Paul's choice. + +## Changed +- Every name in the app now says Orbit Drive-In, including the demo tab, the setup menu, the menu card and the ticket. +- Event types inside it are Orbit Marquee, Orbit Double Bill, Orbit Triple Bill and The Late Show, each with one fictional preview event. Double and Triple Bill are new previews. +- docs/roadmap.md uses the new name. The briefs, older verification notes and design/interface keep the old one as history. + +## Checked +- assembleDebug, testDebugUnitTest and lintDebug pass; 138 tests, 0 failures. +- APK signature verified with apksigner. +- No old name is left in the app source. + +## Not checked +- Not yet run on the box. The Drive-In screen now lists four events instead of two, so its fit needs Paul's on-TV check. diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index 14b6115..7080220 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 = 30 - versionName = "0.29.0-watching-controls" + versionCode = 31 + versionName = "0.30.0-drive-in" } 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 c3d823b..e603e8e 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.29.0\"") + connection.setRequestProperty("X-Emby-Authorization", "Emby Client=\"Orbit TV\", Device=\"Orbit TV\", DeviceId=\"$deviceId\", Version=\"0.30.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/EmbyConnectionScreen.kt b/android/app/src/main/java/tv/orbit/classic/EmbyConnectionScreen.kt index 7e80e04..1f46a2e 100644 --- a/android/app/src/main/java/tv/orbit/classic/EmbyConnectionScreen.kt +++ b/android/app/src/main/java/tv/orbit/classic/EmbyConnectionScreen.kt @@ -51,7 +51,7 @@ import kotlinx.coroutines.withContext @Composable internal fun EmbyConnectionScreen(modifier: Modifier = Modifier, visible: Boolean, mainGuide: Boolean, mainHome: Boolean = false, onChannelAvailable: (Boolean) -> Unit, onGuide: () -> Unit, - onHome: () -> Unit = {}, onOffice: () -> Unit = {}, onBack: () -> Unit) { + onHome: () -> Unit = {}, onDriveIn: () -> Unit = {}, onBack: () -> Unit) { val appContext = androidx.compose.ui.platform.LocalContext.current.applicationContext val savedStore = remember { RememberedConnectionStore.get(appContext) } var setupSaved by remember { mutableStateOf(false) } @@ -242,7 +242,7 @@ internal fun EmbyConnectionScreen(modifier: Modifier = Modifier, visible: Boolea } val picture = @Composable { GuidePicture(liveSession, liveChannel, live = !watchingChannel) } if (mainHome) HomeScreen(availableChannels, liveChannel, modifier, picture, - onCategory = { guideCategory = it; onGuide() }, onWatch = { tune(it, 0) }, onOffice = onOffice) + onCategory = { guideCategory = it; onGuide() }, onWatch = { tune(it, 0) }, onDriveIn = onDriveIn) else GuideGrid(channelsIn(guideCategory, availableChannels).ifEmpty { availableChannels }, selectedNumber, modifier, guideCategory.label, picture, onWatch = ::tune, onBack = { guideCategory = ChannelCategory.All; onHome() }) return diff --git a/android/app/src/main/java/tv/orbit/classic/HomeScreen.kt b/android/app/src/main/java/tv/orbit/classic/HomeScreen.kt index b3a11b4..fab093a 100644 --- a/android/app/src/main/java/tv/orbit/classic/HomeScreen.kt +++ b/android/app/src/main/java/tv/orbit/classic/HomeScreen.kt @@ -67,7 +67,7 @@ private fun ProgressBar(fraction: Float, modifier: Modifier = Modifier, focused: /** Home opens onto the tuned channel playing, one press from anything else. Back takes it full screen. */ @Composable internal fun HomeScreen(channels: List, tuned: SampleChannel, modifier: Modifier = Modifier, - picture: @Composable () -> Unit, onCategory: (ChannelCategory) -> Unit, onWatch: (Int) -> Unit, onOffice: () -> Unit) { + picture: @Composable () -> Unit, onCategory: (ChannelCategory) -> Unit, onWatch: (Int) -> Unit, onDriveIn: () -> Unit) { var now by remember { mutableLongStateOf(System.currentTimeMillis()) } LaunchedEffect(Unit) { while (true) { delay(1000); now = System.currentTimeMillis() } } val firstFocus = remember { FocusRequester() } @@ -120,12 +120,12 @@ internal fun HomeScreen(channels: List, tuned: SampleChannel, mod } } } - val preview = officeEvents.first() - HomeCard(Modifier.fillMaxWidth().weight(1f), onClick = onOffice) { focused -> + val preview = driveInEvents.first() + HomeCard(Modifier.fillMaxWidth().weight(1f), onClick = onDriveIn) { focused -> Column { Row(Modifier.fillMaxWidth().background(if (focused) Yellow else Color(0xFF7A5A12)).padding(horizontal = 10.dp, vertical = 5.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp)) { Image(painterResource(R.drawable.orbit_icon_office), null, Modifier.size(20.dp), colorFilter = if (focused) ColorFilter.tint(Navy) else null) - Text("OrbitOffice preview", color = if (focused) Navy else Color.White, fontSize = 13.sp, fontWeight = FontWeight.SemiBold, maxLines = 1) + Text("Orbit Drive-In preview", color = if (focused) Navy else Color.White, fontSize = 13.sp, fontWeight = FontWeight.SemiBold, maxLines = 1) } Column(Modifier.padding(horizontal = 10.dp, vertical = 8.dp), verticalArrangement = Arrangement.spacedBy(4.dp)) { Text(preview.title, color = if (focused) Navy else Color.White, fontSize = 18.sp, fontWeight = FontWeight.SemiBold, maxLines = 2, overflow = TextOverflow.Ellipsis) 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 b945145..07a5372 100644 --- a/android/app/src/main/java/tv/orbit/classic/MainActivity.kt +++ b/android/app/src/main/java/tv/orbit/classic/MainActivity.kt @@ -132,7 +132,7 @@ private fun OrbitApp() { LaunchedEffect(section, fullscreen, realChannelAvailable) { if (!fullscreen) { if (!realChannelAvailable) { - val tab = when (section) { "TV Guide" -> 1; "Box Office" -> 2; else -> 0 } + val tab = when (section) { "TV Guide" -> 1; "Drive-In" -> 2; else -> 0 } tabFocus[tab].requestFocus() } else if (section != "Home" && section != "TV Guide") gearFocus.requestFocus() } @@ -165,7 +165,7 @@ private fun OrbitApp() { // The numbered tabs only serve the demo guide. With real channels, home and the gear replace them. if (!realChannelAvailable) { Row(horizontalArrangement = Arrangement.spacedBy(6.dp)) { - listOf("Menu", "TV Guide", "Box Office").forEachIndexed { index, title -> + listOf("Menu", "TV Guide", "Drive-In").forEachIndexed { index, title -> OrbitButton("${index + 1} $title", Modifier.focusRequester(tabFocus[index]), section == title, onClick = { section = title; pinned = false }) } @@ -220,8 +220,8 @@ private fun OrbitApp() { OrbitMenu(Modifier.weight(1f), onNavigate = { section = it; pinned = false }) } else if (section == "Connect Emby") { // Connection content is kept composed below to retain the temporary session. - } else if (section == "Box Office") { - OrbOffice(Modifier.weight(1f), onGuide = { section = "TV Guide" }) + } else if (section == "Drive-In") { + DriveIn(Modifier.weight(1f), onGuide = { section = "TV Guide" }) } else if (section != "TV Guide" && section != "Home") { Column(Modifier.fillMaxWidth().weight(1f).background(Panel).padding(28.dp)) { Text("Welcome to Orbit Interactive", color = Yellow, fontSize = 27.sp, fontWeight = FontWeight.Bold) @@ -237,7 +237,7 @@ private fun OrbitApp() { mainGuide = section == "TV Guide" && realChannelAvailable, mainHome = section == "Home" && realChannelAvailable, onChannelAvailable = { realChannelAvailable = it }, - onGuide = { section = "TV Guide" }, onHome = { section = "Home" }, onOffice = { section = "Box Office" }, onBack = { section = "Menu" }) + onGuide = { section = "TV Guide" }, onHome = { section = "Home" }, onDriveIn = { section = "Drive-In" }, onBack = { section = "Menu" }) } } } diff --git a/android/app/src/main/java/tv/orbit/classic/OrbitSections.kt b/android/app/src/main/java/tv/orbit/classic/OrbitSections.kt index 42f8a7a..f6ecb90 100644 --- a/android/app/src/main/java/tv/orbit/classic/OrbitSections.kt +++ b/android/app/src/main/java/tv/orbit/classic/OrbitSections.kt @@ -20,12 +20,16 @@ internal val Barlow = FontFamily(Font(R.font.barlow_regular), Font(R.font.barlow private val TicketType = FontFamily(Font(R.font.barlow_condensed_medium)) private val Paper = Color(0xFFF7EAC8) -internal data class OfficeEvent(val title: String, val collection: String, val time: String, +internal data class DriveInEvent(val title: String, val collection: String, val time: String, val synopsis: String, val reference: String) -internal val officeEvents = listOf( - OfficeEvent("The Last Shuttle Home", "Orbit premieres", "Tonight at 21:30", +internal val driveInEvents = listOf( + DriveInEvent("The Last Shuttle Home", "Orbit Marquee", "Tonight at 21:30", "One final journey brings an unlikely crew together. A fictional premiere for your own front-row seat.", "ORB-001"), - OfficeEvent("Visitors at Dawn", "After dark", "Tonight at 23:00", + DriveInEvent("Twin Moons", "Orbit Double Bill", "Tonight from 20:00", + "Two films, one long night under two moons. A fictional double feature with an interval between.", "ORB-003"), + DriveInEvent("The Far Side Trilogy", "Orbit Triple Bill", "Saturday from 19:00", + "Three chapters back to back, for those who brought snacks. A fictional all-evening event.", "ORB-004"), + DriveInEvent("Visitors at Dawn", "The Late Show", "Tonight at 23:00", "First contact arrives in the quietest corner of Earth. A fictional late-night science-fiction special.", "ORB-002") ) @@ -43,20 +47,20 @@ internal fun OrbitMenu(modifier: Modifier = Modifier, onNavigate: (String) -> Un Column(Modifier.weight(1f).align(Alignment.CenterVertically), verticalArrangement = Arrangement.spacedBy(10.dp)) { OrbitButton("Connect Emby / Check a small sample", Modifier.fillMaxWidth(), onClick = { onNavigate("Connect Emby") }) OrbitButton("TV Guide / What's on now", Modifier.fillMaxWidth(), onClick = { onNavigate("TV Guide") }) - OrbitButton("OrbOffice / Special transmissions", Modifier.fillMaxWidth(), onClick = { onNavigate("Box Office") }) + OrbitButton("Orbit Drive-In / Special transmissions", Modifier.fillMaxWidth(), onClick = { onNavigate("Drive-In") }) OrbitButton("Services / Orbit Interactive", Modifier.fillMaxWidth(), onClick = { onNavigate("Services") }) } } } @Composable -internal fun OrbOffice(modifier: Modifier = Modifier, onGuide: () -> Unit) { +internal fun DriveIn(modifier: Modifier = Modifier, onGuide: () -> Unit) { var selectedEvent by remember { mutableIntStateOf(0) } - val event = officeEvents[selectedEvent] + val event = driveInEvents[selectedEvent] Row(modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(24.dp)) { Column(Modifier.weight(1.55f).fillMaxHeight()) { Row(verticalAlignment = Alignment.Bottom) { - Text("OrbOffice", color = Yellow, fontFamily = TicketType, fontSize = 36.sp) + Text("Orbit Drive-In", color = Yellow, fontFamily = TicketType, fontSize = 36.sp) Text(" Your front-row seat.", color = Muted, fontFamily = Barlow, fontSize = 17.sp, modifier = Modifier.padding(bottom = 5.dp)) } Spacer(Modifier.height(10.dp)) @@ -66,7 +70,7 @@ internal fun OrbOffice(modifier: Modifier = Modifier, onGuide: () -> Unit) { } Column(Modifier.weight(1f).fillMaxHeight(), verticalArrangement = Arrangement.spacedBy(10.dp)) { Text("Special transmissions", color = Color.White, fontFamily = Barlow, fontWeight = FontWeight.SemiBold, fontSize = 23.sp) - officeEvents.forEachIndexed { index, item -> + driveInEvents.forEachIndexed { index, item -> OrbitButton(item.collection, Modifier.fillMaxWidth(), selected = selectedEvent == index, onFocus = { selectedEvent = index }, onClick = { selectedEvent = index }) } @@ -79,7 +83,7 @@ internal fun OrbOffice(modifier: Modifier = Modifier, onGuide: () -> Unit) { } @Composable -private fun AdmissionTicket(event: OfficeEvent, modifier: Modifier = Modifier) { +private fun AdmissionTicket(event: DriveInEvent, modifier: Modifier = Modifier) { Box(modifier.background(Paper)) { Canvas(Modifier.matchParentSize()) { val seam = size.width * .76f @@ -95,7 +99,7 @@ private fun AdmissionTicket(event: OfficeEvent, modifier: Modifier = Modifier) { } Row(Modifier.fillMaxSize()) { Column(Modifier.weight(.76f).fillMaxHeight().padding(horizontal = 22.dp, vertical = 18.dp)) { - Text("OrbOffice", color = Navy, fontFamily = TicketType, fontSize = 27.sp) + Text("Drive-In", color = Navy, fontFamily = TicketType, fontSize = 27.sp) Text(event.collection, color = Navy, fontFamily = Barlow, fontSize = 14.sp) Spacer(Modifier.height(14.dp)) Text(event.title, color = Navy, fontFamily = TicketType, fontSize = 33.sp, lineHeight = 35.sp, maxLines = 3) 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 a218cec..af1ccdc 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.29.0") + next.setRequestProperty("User-Agent", "OrbitTV/0.30.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/docs/roadmap.md b/docs/roadmap.md index d36ad34..63d9024 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -10,7 +10,7 @@ Paul approved these on 1 November 2026. | --- | --- | --- | | 103 name | Installed as "Toonami". The bible has Vortex, and 103's search list also holds titles the bible now gives to Heroes and Orbit X. | Rename the display to Vortex, keep 103 and its lineup. Toonami is a third-party brand, and the creative brief bars those as default branding. | | 101 name | Installed as "Orbit One", bible says "Orbit 1". | Use Orbit 1 on screen, keep 101. | -| Box office and game names | The app and its artwork say OrbOffice and OrbHive. Both briefs say OrbitOffice and OrbitHive. | Use OrbitOffice and OrbitHive going forward. Rename the app and artwork in a later build. | +| Box office and game names | The app and its artwork say OrbOffice and OrbHive. Both briefs say Orbit Drive-In and OrbitHive. | Use Orbit Drive-In and OrbitHive going forward. Rename the app and artwork in a later build. | | Orbit X and Ring X look | Both call for silver or chrome with an aggressive red X, and the bugs will sit on the same screen. | Keep the red X for Orbit X's cartoon slash. Give Ring X a distinct silhouette, decided when its board arrives. | | 102 content | Built from Horror, Thriller and Science Fiction film searches. The bible separates Orbit Horror and defines After Dark as cult and late-night. | Keep 102 as built. Change its rules when presets arrive, and don't claim adult controls before profiles exist. | | Title moves | Toy Story moves from Wonder to PixelZ. Batman Beyond and The Powerpuff Girls drop off Vortex in the new table. | Latest table wins. Batman Beyond stays a permitted crossover and The Powerpuff Girls moves to Rewind. Existing 103 choices stay. | @@ -46,7 +46,7 @@ These are working, with Paul's confirmation noted where given. - The clock-driven schedule stays, and new work plugs into it. - Original Orbit assets only. Concept art is reference, and After Dark is the reference workflow. - Optional, explainable personalisation, with no mandatory AI or cloud service. -- OrbitOffice as occasions, not a catalogue. +- Orbit Drive-In as occasions, not a catalogue. ### Proposed @@ -76,9 +76,9 @@ Paul's ideas, 1 November 2026. Not fixes and not scheduled. - Subtle accent colour per category. - A tiny live pulse or movement on the On now cards, kept gentle and off for reduced motion. - The highlighted left-menu item could reflect the channel or theme, while yellow stays the clear focus signal. -- OrbitOffice gets its own full-screen destination, opened from the teaser card. +- Orbit Drive-In gets its own full-screen destination, opened from the teaser card. - **Subtitles default (Paul, 1 November 2026):** a setting behind the gear for subtitles on or off by default, in the viewer's language where a programme has it. -- **OrbitOffice name (Paul, 1 November 2026):** Paul wants a better name in the spirit of drive-in or matinee. Claude suggested Orbit Drive-In, Marquee, Double Bill, The Late Show or Picture House. Not decided. +- **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. ## Channel numbers @@ -88,7 +88,7 @@ Paul's direction, 1 November 2026: 101, 102 and 103 should become Orbit 1, Orbit Paul's idea, 1 November 2026, for after the 25 channels work. Proposed, not built. -- **Home:** keep the colour scheme and refine the details. Old-Sky-style category list on the left (All Channels, Documentaries, Movies, Kids and so on), showing a category only if the viewer has channels in it. Right side shows what's on now and OrbitOffice events, selectable with the remote. +- **Home:** keep the colour scheme and refine the details. Old-Sky-style category list on the left (All Channels, Documentaries, Movies, Kids and so on), showing a category only if the viewer has channels in it. Right side shows what's on now and Orbit Drive-In events, selectable with the remote. - **Guide:** choosing a category opens its channel list below, with channel name, time and what's on. Live picture-in-picture sits at the top on one side, with the highlighted programme's description on the other. Moving through the list updates the description, and only pressing OK changes the picture. - **Watching controls:** OK shows the classic programme bar. Up and down change channel. Right opens subtitles. Holding OK opens a small box for typing a channel number, because the Xiaomi remote may have no number keys. Left jumps back to the last channel watched. - **Settings:** a gear at the top right holds connection, profiles, themes and editing, so setup stays out of the way. @@ -106,7 +106,7 @@ flowchart TD cat --> list["Generic channel list"] preset --> rec["P5 Recommendations"] list --> grid["Grid guide restored"] --> brand["P4 Branding rollout"] - list --> sched["P3 Schedule quality"] --> office["P6 OrbitOffice"] + list --> sched["P3 Schedule quality"] --> office["P6 Orbit Drive-In"] ``` Gate before mature channels reach other households: profiles and enforced ratings. @@ -124,7 +124,7 @@ The clock engine stays. These limits are the parts that have to change, and only | Queries capped at 20 items, by hand | No library-wide view | P1 | | 512 KiB saved state, fixed channel slots, numbers 101 to 103 only | Can't store a catalogue or a fourth channel | P1, channel list | | Channel lineups of 2 to 15 unique titles | Too small for real channels, and blocks intentional repeats | P3 | -| One repeating loop anchored to UTC | No concept of weekday, evening, local time or daylight saving | Dark Files, Ring X, OrbitOffice | +| One repeating loop anchored to UTC | No concept of weekday, evening, local time or daylight saving | Dark Files, Ring X, Orbit Drive-In | For that last row, I'd keep the loop as the default and add an optional local-time block layer that hands each block a lineup. It needs its own design and your approval before any work starts. That also meets your rule against replacing the engine. @@ -140,7 +140,7 @@ For that last row, I'd keep the loop as the default and add an optional local-ti | Grid guide with real Now and Next | After channel list | | Bottom-left bug, off by default | P4 | | Optional idents with reduced-motion setting, never delaying the clock | P4 | -| Dashboard: On now, Coming up, OrbitOffice presents | With P5 and P6 | +| Dashboard: On now, Coming up, Orbit Drive-In presents | With P5 and P6 | | First-run onboarding and more providers | Later, Emby preserved | | Profiles, Kids, PIN | Later, with the gate shown above | | Themes, Catch Up, Games, OrbText | Later, unchanged |