Rename OrbitOffice to Orbit Drive-In, with its event types (0.30.0)
pauljd/davidson-ci: Davidson CI / quality (push) Failing after 1m46s
pauljd/davidson-ci: Davidson CI / ai-review (push) Failing after 1m20s

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-11-01 23:09:16 +00:00
co-authored by Claude Opus 5.5
parent c423af00a6
commit e69606950f
9 changed files with 54 additions and 34 deletions
+16
View File
@@ -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.
+2 -2
View File
@@ -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 }
@@ -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)
@@ -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
@@ -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<SampleChannel>, 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<SampleChannel>, 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)
@@ -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" })
}
}
}
@@ -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)
@@ -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()) {
+8 -8
View File
@@ -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 |