From a534aed53c15cba0c04aa5d55301a41352168490 Mon Sep 17 00:00:00 2001 From: Pauljd Date: Mon, 2 Nov 2026 11:01:27 +0000 Subject: [PATCH] Add Detour, BritX, Orbit Junior and Orbit Wild, and a subtitles and audio panel (0.63.0) Co-Authored-By: Claude Opus 5.5 (1M context) --- VERIFICATION-0.63.0.md | 18 ++++++ android/app/build.gradle.kts | 4 +- .../java/tv/orbit/classic/ChannelBranding.kt | 4 ++ .../java/tv/orbit/classic/ChannelPresets.kt | 21 ++++++- .../java/tv/orbit/classic/ChannelSchedule.kt | 3 +- .../tv/orbit/classic/ClassicProgrammeStrip.kt | 2 +- .../main/java/tv/orbit/classic/EmbyClient.kt | 2 +- .../tv/orbit/classic/EmbyPlaybackDialog.kt | 59 +++++++++++++----- .../tv/orbit/classic/SampleChannelScreens.kt | 3 +- .../tv/orbit/classic/SecureMediaDataSource.kt | 2 +- .../java/tv/orbit/classic/WatchingControls.kt | 37 ++++++++--- .../res/drawable-nodpi/orbit_britx_bug.webp | Bin 0 -> 3202 bytes .../res/drawable-nodpi/orbit_britx_epg.webp | Bin 0 -> 6254 bytes .../res/drawable-nodpi/orbit_detour_bug.webp | Bin 0 -> 4368 bytes .../res/drawable-nodpi/orbit_detour_epg.webp | Bin 0 -> 8088 bytes .../res/drawable-nodpi/orbit_junior_bug.webp | Bin 0 -> 3482 bytes .../res/drawable-nodpi/orbit_junior_epg.webp | Bin 0 -> 6414 bytes .../res/drawable-nodpi/orbit_wild_bug.webp | Bin 0 -> 3160 bytes .../res/drawable-nodpi/orbit_wild_epg.webp | Bin 0 -> 5614 bytes .../tv/orbit/classic/ChannelPresetsTest.kt | 18 ++++++ .../tv/orbit/classic/WatchingControlsTest.kt | 17 ++--- design/channels/build-more-brands.py | 57 +++++++++++++++++ .../channels/orbit-britx/orbit_britx_bug.svg | 1 + .../channels/orbit-britx/orbit_britx_epg.svg | 1 + .../orbit-detour/orbit_detour_bug.svg | 1 + .../orbit-detour/orbit_detour_epg.svg | 1 + .../orbit-junior/orbit_junior_bug.svg | 1 + .../orbit-junior/orbit_junior_epg.svg | 1 + design/channels/orbit-wild/orbit_wild_bug.svg | 1 + design/channels/orbit-wild/orbit_wild_epg.svg | 1 + docs/roadmap.md | 2 +- 31 files changed, 213 insertions(+), 44 deletions(-) create mode 100644 VERIFICATION-0.63.0.md create mode 100644 android/app/src/main/res/drawable-nodpi/orbit_britx_bug.webp create mode 100644 android/app/src/main/res/drawable-nodpi/orbit_britx_epg.webp create mode 100644 android/app/src/main/res/drawable-nodpi/orbit_detour_bug.webp create mode 100644 android/app/src/main/res/drawable-nodpi/orbit_detour_epg.webp create mode 100644 android/app/src/main/res/drawable-nodpi/orbit_junior_bug.webp create mode 100644 android/app/src/main/res/drawable-nodpi/orbit_junior_epg.webp create mode 100644 android/app/src/main/res/drawable-nodpi/orbit_wild_bug.webp create mode 100644 android/app/src/main/res/drawable-nodpi/orbit_wild_epg.webp create mode 100644 design/channels/build-more-brands.py create mode 100644 design/channels/orbit-britx/orbit_britx_bug.svg create mode 100644 design/channels/orbit-britx/orbit_britx_epg.svg create mode 100644 design/channels/orbit-detour/orbit_detour_bug.svg create mode 100644 design/channels/orbit-detour/orbit_detour_epg.svg create mode 100644 design/channels/orbit-junior/orbit_junior_bug.svg create mode 100644 design/channels/orbit-junior/orbit_junior_epg.svg create mode 100644 design/channels/orbit-wild/orbit_wild_bug.svg create mode 100644 design/channels/orbit-wild/orbit_wild_epg.svg diff --git a/VERIFICATION-0.63.0.md b/VERIFICATION-0.63.0.md new file mode 100644 index 0000000..a673fdd --- /dev/null +++ b/VERIFICATION-0.63.0.md @@ -0,0 +1,18 @@ +# Orbit TV 0.62.0 and 0.63.0 verification + +English sound, four more channels and a subtitles and audio panel. + +## Changed +- 0.62: asks for English sound, and leaves out episodes with only other languages, except on Vortex. +- 0.63: Detour (114) and BritX (115) film channels from Paul's lists, each film resting a week. Orbit Junior (125) in the Kids category and Orbit Wild (143). New vector logos in design/channels/build-more-brands.py, with an amber X on BritX so it can't be mistaken for Orbit X. +- Right while watching opens a panel of subtitles and audio. Choices carry across programmes until you leave full screen. Up to 8 subtitles and 5 audio tracks are listed. + +## Checked +- assembleDebug, testDebugUnitTest and lintDebug pass; 250 tests, 0 failures. +- Logos checked on the PC at the size they appear, not on the TV. +- APK signature verified with apksigner. + +## Not checked +- Not yet run on the box. Which titles Paul's library has, and the panel on a real remote, can only be seen there. +- Orbit Junior is hand-picked but isn't age-locked, as profiles don't exist yet. +- I filled out Junior's and Wild's lists myself beyond Paul's named shows. diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index b902d46..6d28539 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 = 63 - versionName = "0.62.0-english-audio" + versionCode = 64 + versionName = "0.63.0-four-more-channels" } buildFeatures { compose = true } compileOptions { sourceCompatibility = JavaVersion.VERSION_17; targetCompatibility = JavaVersion.VERSION_17 } diff --git a/android/app/src/main/java/tv/orbit/classic/ChannelBranding.kt b/android/app/src/main/java/tv/orbit/classic/ChannelBranding.kt index 802ab81..601990f 100644 --- a/android/app/src/main/java/tv/orbit/classic/ChannelBranding.kt +++ b/android/app/src/main/java/tv/orbit/classic/ChannelBranding.kt @@ -11,6 +11,10 @@ internal object ChannelBrands { 112 to ChannelBranding("orbit-three", "Orbit Three", R.drawable.orbit_three_epg, R.drawable.orbit_three_background, R.drawable.orbit_three_bug, ChannelCategory.Comedy), 113 to ChannelBranding("orbit-riot", "Orbit Riot", R.drawable.orbit_riot_epg, R.drawable.orbit_riot_background, R.drawable.orbit_riot_bug, ChannelCategory.Comedy), 142 to ChannelBranding("orbit-dark-files", "Orbit Dark Files", R.drawable.orbit_dark_files_epg, R.drawable.orbit_dark_files_background, R.drawable.orbit_dark_files_bug, ChannelCategory.Documentaries), + 114 to ChannelBranding("orbit-detour", "Orbit Detour", R.drawable.orbit_detour_epg, R.drawable.orbit_one_background, R.drawable.orbit_detour_bug, ChannelCategory.Movies), + 115 to ChannelBranding("orbit-britx", "Orbit BritX", R.drawable.orbit_britx_epg, R.drawable.orbit_one_background, R.drawable.orbit_britx_bug, ChannelCategory.Movies), + 125 to ChannelBranding("orbit-junior", "Orbit Junior", R.drawable.orbit_junior_epg, R.drawable.orbit_one_background, R.drawable.orbit_junior_bug, ChannelCategory.Kids), + 143 to ChannelBranding("orbit-wild", "Orbit Wild", R.drawable.orbit_wild_epg, R.drawable.orbit_one_background, R.drawable.orbit_wild_bug, ChannelCategory.Documentaries), 121 to ChannelBranding("heroes", "Orbit Heroes", R.drawable.heroes_epg, R.drawable.heroes_background, R.drawable.heroes_bug, ChannelCategory.Cartoons), 122 to ChannelBranding("toonz", "Orbit ToonZ", R.drawable.toonz_epg, R.drawable.toonz_background, R.drawable.toonz_bug, ChannelCategory.Cartoons), 123 to ChannelBranding("rewind", "Orbit Rewind", R.drawable.rewind_epg, R.drawable.rewind_background, R.drawable.rewind_bug, ChannelCategory.Cartoons), diff --git a/android/app/src/main/java/tv/orbit/classic/ChannelPresets.kt b/android/app/src/main/java/tv/orbit/classic/ChannelPresets.kt index d1ab0d2..46ad42d 100644 --- a/android/app/src/main/java/tv/orbit/classic/ChannelPresets.kt +++ b/android/app/src/main/java/tv/orbit/classic/ChannelPresets.kt @@ -3,7 +3,9 @@ package tv.orbit.classic /** A channel the app can build from named shows. Numbers follow the design and may change with the generic channel list. */ internal data class ChannelPreset(val number: Int, val name: String, val searches: List, /** Started from its smart schedule alone, with no separate loop to build first. */ - val smartOnly: Boolean = false) + val smartOnly: Boolean = false, + /** A film channel: curated titles, each resting a week between showings. */ + val films: Boolean = false) internal val DarkFilesCrime = listOf("Forensic Files", "Cold Case Files", "The First 48", "Making a Murderer", "The Staircase", "The Jinx", "I Am a Killer", "Evil Lives Here", "Conversations with a Killer: The Ted Bundy Tapes", "Crime Scene: The Vanishing at the Cecil Hotel", "Snapped", "American Justice", "City Confidential", "Homicide Hunter", "The Keepers", "Don't F**k with Cats: Hunting an Internet Killer", "The Confession Tapes", "Night Stalker: The Hunt for a Serial Killer", @@ -39,12 +41,25 @@ internal val ChannelPresets = listOf( ChannelPreset(141, "Evidence", listOf("CSI: Crime Scene Investigation", "NCIS", "CSI: Miami", "CSI: NY", "Law & Order", "Law & Order: Special Victims Unit", "Criminal Minds", "Bones", "Castle", "The Mentalist", "Monk", "Columbo", "Midsomer Murders", "Vera", "Luther", "Line of Duty", "Taggart", "Silent Witness", "Waking the Dead", "A Touch of Frost", "Dexter", "Sherlock", "Broadchurch", "Death in Paradise", "Lewis", "Inspector Morse", "Dalziel and Pascoe", "Happy Valley", "The Wire", "Hawaii Five-0", "Blue Bloods"), smartOnly = true), - ChannelPreset(142, "Dark Files", DarkFilesCrime + DarkFilesMysteries + DarkFilesParanormal, smartOnly = true)) + ChannelPreset(142, "Dark Files", DarkFilesCrime + DarkFilesMysteries + DarkFilesParanormal, smartOnly = true), + ChannelPreset(114, "Detour", listOf("American Pie", "American Pie 2", "American Wedding", "American Reunion", "Road Trip", "EuroTrip", "Harold & Kumar Go to White Castle", "Harold & Kumar Escape from Guantanamo Bay", + "A Very Harold & Kumar Christmas", "The Hangover", "The Hangover Part II", "The Hangover Part III", "Van Wilder", "Van Wilder: The Rise of Taj", "Old School", "Sex Drive", "Accepted", "Superbad", "Pineapple Express", + "Dude, Where's My Car?", "Without a Paddle", "The Girl Next Door", "Not Another Teen Movie", "Project X", "21 & Over", "Beerfest", "Grandma's Boy", "Waiting..."), smartOnly = true, films = true), + ChannelPreset(115, "BritX", listOf("Shaun of the Dead", "Hot Fuzz", "The World's End", "Lock, Stock and Two Smoking Barrels", "Snatch", "RocknRolla", "Layer Cake", "Trainspotting", "T2 Trainspotting", "Green Street", + "The Football Factory", "Dog Soldiers", "Dead Man's Shoes", "28 Days Later", "28 Weeks Later", "This Is England", "The Gentlemen", "The Business", "The Firm", "Rise of the Footsoldier", "The 51st State", "Sexy Beast", + "The Bank Job", "The Long Good Friday", "Human Traffic", "Twin Town", "Shallow Grave", "Four Lions", "Severance", "Attack the Block", "The Descent", "In Bruges", "Filth", "Calibre", "Sightseers", "Sweet Sixteen"), smartOnly = true, films = true), + ChannelPreset(125, "Orbit Junior", listOf("Thomas the Tank Engine & Friends", "Thomas & Friends", "Fireman Sam", "Postman Pat", "Brum", "Rosie and Jim", "Bob the Builder", "Pingu", "Noddy", "Teletubbies", "Tweenies", "Tots TV", + "Pingu in the City", "Peppa Pig", "Bluey", "Bagpuss", "The Clangers", "Camberwick Green", "Trumpton", "Button Moon", "Rainbow", "Balamory", "Fimbles"), smartOnly = true), + ChannelPreset(143, "Orbit Wild", listOf("The Crocodile Hunter", "Crocodile Hunter Diaries", "Deadliest Catch", "River Monsters", "Man vs. Wild", "Running Wild with Bear Grylls", "Survivorman", "Dual Survival", "Naked and Afraid", "Ice Road Truckers", + "Swamp People", "Wild Things with Dominic Monaghan", "Lone Star Law", "Alone", "Ax Men", "Monster Fish", "Dirty Jobs", "Shark Week", "Whale Wars", "Dangerous Encounters with Brady Barr"), smartOnly = true)) /** A fallback for a channel with no old loop: its first episodes, in turn. Used only outside its plan. */ internal fun loopFromPool(pool: List, programming: ChannelProgramming): SampleChannel? { val turns = (0 until (pool.maxOfOrNull { it.episodes.size } ?: 0)).asSequence().flatMap { round -> pool.asSequence().mapNotNull { show -> show.episodes.getOrNull(round)?.let { show to it } } } - val media = turns.take(15).map { (show, episode) -> ScheduledMedia(scheduledItem(show.title, episode), episode.sourceId, episode.durationMs) }.toList() + val media = turns.take(15).map { (show, episode) -> + val item = if (show.isFilm) SampleItem(show.title, "Movie", episode.durationMs / 60_000, "", episode.id, episode.durationMs) else scheduledItem(show.title, episode) + ScheduledMedia(item, episode.sourceId, episode.durationMs) + }.toList() return if (media.size < 2) null else SampleChannel(media, programming.name, programming.number) } diff --git a/android/app/src/main/java/tv/orbit/classic/ChannelSchedule.kt b/android/app/src/main/java/tv/orbit/classic/ChannelSchedule.kt index f00b6fe..03a34d8 100644 --- a/android/app/src/main/java/tv/orbit/classic/ChannelSchedule.kt +++ b/android/app/src/main/java/tv/orbit/classic/ChannelSchedule.kt @@ -59,7 +59,8 @@ internal val SmartChannels = listOf( "Hostile Planet", "Africa", "Wild China", "South Pacific", "Great Barrier Reef", "The Hunt", "The Life of Mammals", "The Life of Birds", "Nature's Great Events", "Green Planet", "Prehistoric Planet"), seriesCount = 20, finishTogether = true, exactTitles = true), ChannelProgramming(152, "Paradox", ChannelPresets.first { it.number == 152 }.searches)) + - ChannelPresets.filter { it.smartOnly }.map { ChannelProgramming(it.number, it.name, it.searches, seriesCount = 60, finishTogether = true, exactTitles = true, + ChannelPresets.filter { it.smartOnly }.map { ChannelProgramming(it.number, it.name, it.searches, seriesCount = 60, finishTogether = !it.films, exactTitles = true, + filmRestMs = if (it.films) 7 * 24 * 60 * 60_000L else null, suggestionRules = SmartChannelRules[it.number].orEmpty(), dayParts = SmartChannelParts[it.number].orEmpty()) } /** Dark Files, from the programming bible: crime by day, mysteries early evening, the paranormal late. */ diff --git a/android/app/src/main/java/tv/orbit/classic/ClassicProgrammeStrip.kt b/android/app/src/main/java/tv/orbit/classic/ClassicProgrammeStrip.kt index d7ffc1b..aa397c8 100644 --- a/android/app/src/main/java/tv/orbit/classic/ClassicProgrammeStrip.kt +++ b/android/app/src/main/java/tv/orbit/classic/ClassicProgrammeStrip.kt @@ -46,7 +46,7 @@ internal fun ClassicProgrammeStrip(item: SampleItem, playing: Boolean, modifier: Legend("↶", "TV guide") if (scheduled != null) { Legend("◄", "Last channel") - Legend("►", "Subtitles") + Legend("►", "Subtitles and audio") Legend("Hold OK", "Number") } } 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 1ee8e7c..82c7b7c 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.62.0\"") + connection.setRequestProperty("X-Emby-Authorization", "Emby Client=\"Orbit TV\", Device=\"Orbit TV\", DeviceId=\"$deviceId\", Version=\"0.63.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/EmbyPlaybackDialog.kt b/android/app/src/main/java/tv/orbit/classic/EmbyPlaybackDialog.kt index 333fa82..adf837b 100644 --- a/android/app/src/main/java/tv/orbit/classic/EmbyPlaybackDialog.kt +++ b/android/app/src/main/java/tv/orbit/classic/EmbyPlaybackDialog.kt @@ -41,7 +41,7 @@ private const val MAX_PLAYER_RETRIES = 1 @androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class) @Composable internal fun EmbyPlaybackDialog(session: EmbySampleSession, plan: EmbyPlaybackPlan, onClose: () -> Unit, scheduled: ScheduledPlayback? = null, - onStep: (Int) -> Unit = {}, onLast: () -> Unit = {}, onNumber: (Int) -> Boolean = { false }, subtitlePreference: MutableState? = null, runningPlayer: ExoPlayer? = null, announce: Boolean = true, onHandBack: ((ExoPlayer) -> Boolean)? = null, onUnplayable: ((String) -> Unit)? = null) { + onStep: (Int) -> Unit = {}, onLast: () -> Unit = {}, onNumber: (Int) -> Boolean = { false }, subtitlePreference: MutableState? = null, runningPlayer: ExoPlayer? = null, announce: Boolean = true, onHandBack: ((ExoPlayer) -> Boolean)? = null, onUnplayable: ((String) -> Unit)? = null, audioPreference: MutableState? = null) { val context = LocalContext.current var failure by remember { mutableStateOf(null) } var tuningNumberVisible by remember { mutableStateOf(announce) } @@ -57,7 +57,8 @@ internal fun EmbyPlaybackDialog(session: EmbySampleSession, plan: EmbyPlaybackPl var numberProblem by remember { mutableStateOf(null) } var okHeld by remember { mutableStateOf(false) } var subtitleIndex by remember { mutableStateOf(null) } - var subtitleNote by remember { mutableStateOf(null) } + var trackMenu by remember { mutableStateOf?>(null) } + var trackCursor by remember { mutableIntStateOf(0) } val player = remember(session, plan) { if (runningPlayer != null) return@remember runningPlayer val factory = DataSource.Factory { SecureMediaDataSource(session) } @@ -65,7 +66,7 @@ internal fun EmbyPlaybackDialog(session: EmbySampleSession, plan: EmbyPlaybackPl .setSeekBackIncrementMs(10_000).setSeekForwardIncrementMs(10_000).build().apply { setMediaItem(channelMediaItem(plan)) if (scheduled != null) seekTo(scheduled.programme.offsetAt(scheduled.nowMs())) - trackSelectionParameters = trackSelectionParameters.buildUpon().setPreferredAudioLanguage("en").build() + trackSelectionParameters = trackSelectionParameters.buildUpon().setPreferredAudioLanguage(audioPreference?.value ?: "en").build() prepare(); playWhenReady = true } } @@ -76,17 +77,25 @@ internal fun EmbyPlaybackDialog(session: EmbySampleSession, plan: EmbyPlaybackPl if (index != null) builder.addOverride(TrackSelectionOverride(tracks[index].mediaTrackGroup, 0)) player.trackSelectionParameters = builder.build() } - fun cycleSubtitles() { - val tracks = textTracks() - val choice = nextSubtitle(subtitleIndex, tracks.size) - chooseSubtitle(choice, tracks) - val format = choice?.let { tracks[it].getTrackFormat(0) } - subtitlePreference?.value = format?.let { it.language.orEmpty() } - subtitleNote = when { - tracks.isEmpty() -> "No subtitles on this programme" - format == null -> "Subtitles off" - else -> "Subtitles: ${format.label ?: format.language ?: "Track ${choice + 1}"}" + fun chooseAudio(group: Tracks.Group) { + player.trackSelectionParameters = player.trackSelectionParameters.buildUpon().clearOverridesOfType(C.TRACK_TYPE_AUDIO) + .addOverride(TrackSelectionOverride(group.mediaTrackGroup, 0)).build() + audioPreference?.value = group.getTrackFormat(0).language + } + fun trackRows(): List { + val subtitles = textTracks() + val audio = player.currentTracks.groups.filter { it.type == C.TRACK_TYPE_AUDIO && it.isSupported } + val subtitleRows = listOf(TrackRow("Subtitles", "Off", subtitles.none { it.isSelected }) { chooseSubtitle(null, subtitles); subtitlePreference?.value = null }) + + // Capped so the panel always fits the screen, even on a file with dozens of subtitles. + subtitles.take(8).mapIndexed { index, group -> + val format = group.getTrackFormat(0) + TrackRow("Subtitles", trackLabel(format.label, format.language, index), group.isSelected) { chooseSubtitle(index, subtitles); subtitlePreference?.value = format.language.orEmpty() } + } + val audioRows = audio.take(5).mapIndexed { index, group -> + val format = group.getTrackFormat(0) + TrackRow("Audio", trackLabel(format.label, format.language, index), group.isSelected) { chooseAudio(group) } } + return subtitleRows + audioRows } fun submitNumber(entry: ChannelEntry) { if (onNumber(entry.number)) { numberEntry = null; numberProblem = null } @@ -100,7 +109,8 @@ internal fun EmbyPlaybackDialog(session: EmbySampleSession, plan: EmbyPlaybackPl } } LaunchedEffect(numberEntry) { if (numberEntry != null) { delay(10_000); numberEntry = null; numberProblem = null } } - LaunchedEffect(subtitleNote) { if (subtitleNote != null) { delay(3_000); subtitleNote = null } } + // Closes by itself, so a stray press doesn't leave it over the programme. + LaunchedEffect(trackMenu, trackCursor) { if (trackMenu != null) { delay(10_000); trackMenu = null } } val lifecycle = LocalLifecycleOwner.current.lifecycle var foreground by remember { mutableStateOf(lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED)) } DisposableEffect(player, lifecycle) { @@ -195,7 +205,7 @@ internal fun EmbyPlaybackDialog(session: EmbySampleSession, plan: EmbyPlaybackPl LaunchedEffect(infoVisible, lastInteraction, playing, ready) { if (infoVisible && playing && ready) { delay(5_000); infoVisible = false } } - Dialog(onDismissRequest = { if (numberEntry != null) { numberEntry = null; numberProblem = null } else { + Dialog(onDismissRequest = { if (trackMenu != null) trackMenu = null else if (numberEntry != null) { numberEntry = null; numberProblem = null } else { // Backing out keeps the stream running in the picture, without subtitles at that size. if (ready && failure == null && foreground && onHandBack?.invoke(player) == true) { handedBack = true @@ -223,6 +233,17 @@ internal fun EmbyPlaybackDialog(session: EmbySampleSession, plan: EmbyPlaybackPl if (event.action == KeyEvent.ACTION_UP) okHeld = false return@setOnKeyListener true } + trackMenu?.let { rows -> + if (event.action == KeyEvent.ACTION_DOWN) when (code) { + KeyEvent.KEYCODE_DPAD_UP -> trackCursor = (trackCursor - 1).coerceAtLeast(0) + KeyEvent.KEYCODE_DPAD_DOWN -> trackCursor = (trackCursor + 1).coerceAtMost(rows.lastIndex) + } + if (event.action == KeyEvent.ACTION_UP) { + if (isOk) { rows[trackCursor].choose(); trackMenu = null } + else if (code == KeyEvent.KEYCODE_DPAD_LEFT || code == KeyEvent.KEYCODE_DPAD_RIGHT) trackMenu = null + } + return@setOnKeyListener isOk || code in listOf(KeyEvent.KEYCODE_DPAD_UP, KeyEvent.KEYCODE_DPAD_DOWN, KeyEvent.KEYCODE_DPAD_LEFT, KeyEvent.KEYCODE_DPAD_RIGHT) + } numberEntry?.let { entry -> // On release, so the key that closes the box can't also act on the channel behind it. val advance = (isOk || code == KeyEvent.KEYCODE_DPAD_RIGHT) && event.action == KeyEvent.ACTION_UP @@ -254,7 +275,11 @@ internal fun EmbyPlaybackDialog(session: EmbySampleSession, plan: EmbyPlaybackPl KeyEvent.KEYCODE_DPAD_DOWN, KeyEvent.KEYCODE_CHANNEL_DOWN -> if (scheduled != null) onStep(-1) KeyEvent.KEYCODE_DPAD_LEFT -> if (scheduled != null) onLast() else player.seekBack() KeyEvent.KEYCODE_MEDIA_REWIND -> if (scheduled == null) player.seekBack() - KeyEvent.KEYCODE_DPAD_RIGHT -> if (scheduled != null) cycleSubtitles() else player.seekForward() + KeyEvent.KEYCODE_DPAD_RIGHT -> if (scheduled != null) { + val rows = trackRows() + trackCursor = rows.indexOfFirst { it.selected }.coerceAtLeast(0) + trackMenu = rows + } else player.seekForward() KeyEvent.KEYCODE_MEDIA_FAST_FORWARD -> if (scheduled == null) player.seekForward() KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE -> if (scheduled == null) { if (player.isPlaying) player.pause() else player.play() } KeyEvent.KEYCODE_MEDIA_PLAY -> player.play() @@ -282,7 +307,7 @@ internal fun EmbyPlaybackDialog(session: EmbySampleSession, plan: EmbyPlaybackPl Image(painterResource(bug), null, Modifier.align(Alignment.BottomStart).padding(start = 40.dp, bottom = 28.dp).width(108.dp).height(36.dp), alpha = .8f) if (infoVisible && failure == null && ready) ClassicProgrammeStrip(plan.item, playing, Modifier.align(Alignment.BottomCenter).fillMaxWidth(.86f).padding(bottom = 26.dp), scheduled = scheduled) - subtitleNote?.let { Text(it, Modifier.align(Alignment.TopCenter).padding(top = 28.dp).background(Navy).padding(horizontal = 14.dp, vertical = 6.dp), color = Color.White, fontSize = 15.sp) } + trackMenu?.let { TrackPanel(it, trackCursor, Modifier.align(Alignment.CenterEnd).padding(end = 28.dp)) } numberEntry?.let { ChannelNumberBox(it, numberProblem, Modifier.align(Alignment.TopCenter).padding(top = 44.dp)) } if (failure != null) Column(Modifier.align(Alignment.Center).widthIn(max = 620.dp).background(Navy).padding(24.dp)) { Text(failure.orEmpty(), color = Color.White, fontSize = 20.sp) diff --git a/android/app/src/main/java/tv/orbit/classic/SampleChannelScreens.kt b/android/app/src/main/java/tv/orbit/classic/SampleChannelScreens.kt index ac4618b..7951f1b 100644 --- a/android/app/src/main/java/tv/orbit/classic/SampleChannelScreens.kt +++ b/android/app/src/main/java/tv/orbit/classic/SampleChannelScreens.kt @@ -93,6 +93,7 @@ internal fun ScheduledChannelPlayer(session: EmbySampleSession, channel: SampleC onClose: () -> Unit, onStep: (Int) -> Unit = {}, onLast: () -> Unit = {}, onNumber: (Int) -> Boolean = { false }, handoff: LiveHandoff? = null, onUnplayable: ((String) -> Unit)? = null) { // Outlives each programme and channel, and ends when the viewer leaves full screen. val subtitlePreference = remember { mutableStateOf(null) } + val audioPreference = remember { mutableStateOf(null) } var now by remember { mutableLongStateOf(System.currentTimeMillis() + clockOffsetMs) } LaunchedEffect(channel, clockOffsetMs) { while (true) { now = System.currentTimeMillis() + clockOffsetMs; delay(250) } } val shows = channel.guide(now, 2) @@ -136,7 +137,7 @@ internal fun ScheduledChannelPlayer(session: EmbySampleSession, channel: SampleC val announce = remember { announcedChannel[0] != channel.number } SideEffect { announcedChannel[0] = channel.number } EmbyPlaybackDialog(session, plan, onClose, - scheduled = ScheduledPlayback(channel, current, shows[1], clockOffsetMs), onStep = onStep, onLast = onLast, onNumber = onNumber, subtitlePreference = subtitlePreference, runningPlayer = handed?.takeIf { it.plan === plan }?.claim(), announce = announce, onUnplayable = onUnplayable, + scheduled = ScheduledPlayback(channel, current, shows[1], clockOffsetMs), onStep = onStep, onLast = onLast, onNumber = onNumber, subtitlePreference = subtitlePreference, runningPlayer = handed?.takeIf { it.plan === plan }?.claim(), announce = announce, onUnplayable = onUnplayable, audioPreference = audioPreference, onHandBack = { running -> if (handoff != null && clockOffsetMs == 0L) { handoff.returned = LiveStream(channel.number, current.startMs, plan, running); true } else false }) 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 2d6de58..c1ac07f 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.62.0") + next.setRequestProperty("User-Agent", "OrbitTV/0.63.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/android/app/src/main/java/tv/orbit/classic/WatchingControls.kt b/android/app/src/main/java/tv/orbit/classic/WatchingControls.kt index c04c994..8351e35 100644 --- a/android/app/src/main/java/tv/orbit/classic/WatchingControls.kt +++ b/android/app/src/main/java/tv/orbit/classic/WatchingControls.kt @@ -27,14 +27,6 @@ internal data class ChannelEntry(val digits: List, val slot: Int = 0) { } } -/** Right steps through the tracks and then off. */ -internal fun nextSubtitle(current: Int?, count: Int): Int? = when { - count == 0 -> null - current == null -> 0 - current + 1 < count -> current + 1 - else -> null -} - /** Subtitles stay on across programmes: the same language if it's there, otherwise the first track. */ internal fun subtitleFor(wanted: String?, languages: List): Int? { if (wanted == null || languages.isEmpty()) return null @@ -57,3 +49,32 @@ internal fun ChannelNumberBox(entry: ChannelEntry, problem: String?, modifier: M Text(problem ?: "▲▼ Change OK Next Back Cancel", color = if (problem != null) Yellow else Muted, fontSize = 11.sp) } } + +/** One choice in the subtitles and audio panel. */ +internal class TrackRow(val section: String, val label: String, val selected: Boolean, val choose: () -> Unit) + +/** A plain name for a track: its own label, else its language, else its position. */ +internal fun trackLabel(label: String?, language: String?, position: Int): String { + label?.trim()?.takeIf { it.isNotEmpty() }?.let { return it } + val code = language?.trim().orEmpty() + if (code.isNotEmpty() && code != "und") { + val name = java.util.Locale(code).getDisplayLanguage(java.util.Locale.UK) + return if (name.isNullOrBlank() || name == code) code else name + } + return "Track ${position + 1}" +} + +/** Sits to one side, so the programme keeps playing while you choose. */ +@Composable +internal fun TrackPanel(rows: List, cursor: Int, modifier: Modifier = Modifier) { + val corner = RoundedCornerShape(6.dp) + Column(modifier.width(280.dp).background(Navy.copy(alpha = .94f), corner).border(1.dp, Panel, corner).padding(12.dp), verticalArrangement = Arrangement.spacedBy(4.dp)) { + rows.forEachIndexed { index, row -> + if (index == 0 || rows[index - 1].section != row.section) Text(row.section, color = Muted, fontSize = 13.sp, fontWeight = FontWeight.SemiBold, modifier = Modifier.padding(top = if (index == 0) 0.dp else 6.dp)) + val on = index == cursor + Text((if (row.selected) "✓ " else " ") + row.label, Modifier.fillMaxWidth().background(if (on) Yellow else Color.Transparent, RoundedCornerShape(4.dp)).padding(horizontal = 8.dp, vertical = 4.dp), + color = if (on) Navy else Color.White, fontSize = 15.sp, fontWeight = if (on || row.selected) FontWeight.SemiBold else FontWeight.Normal, maxLines = 1) + } + Text("▲▼ Move OK Choose Back Close", color = Muted, fontSize = 11.sp, modifier = Modifier.padding(top = 4.dp)) + } +} diff --git a/android/app/src/main/res/drawable-nodpi/orbit_britx_bug.webp b/android/app/src/main/res/drawable-nodpi/orbit_britx_bug.webp new file mode 100644 index 0000000000000000000000000000000000000000..cc9fa8307cf44251b1683a1337a4b77ab71b93bb GIT binary patch literal 3202 zcmV-|41M!bNk&F`3;+OEMM6+kP&il$0000G0001w001ul06|PpNPq4~ z%W1RLiA_yYnr=UNX=%IG@vZyyXqntpJGrHI&z4EeH_hd5Y@T%IFxk>r(_==FbCtYN zm%U4eXG=HH;a7{R88~knI$mj2fO!W3ELA4mNy?kES*!Mq^WK|I7|r!n$O42c=F8~O zkcKSDL&AZ2CCmuh9GOfHA%rCPeeMAv4}a7j8m8xW%c`ta140qP7&*gf6aFG^v<=gW*gOq3dF68`L*?^p)f8{GOHeH{L_e`BXm6UhueMO5+htDUIa)5kkoJNv! zfqbW2U1g8A&h5x)JZ)U5OKFRb>j*M_(h6xTSpEnS?o&m#k@AqN(L8^^pFb1Rm1Ik_ z4jJ3dgsk+$FxhsDG0&25-{HSUSXbAP@yD63k{92Y+i2r*RrD+=SI8O7^D7Yo&&a4m z>k=3%*C{Xa&=Im~PqTkb1c=}4Bt1x={k`-Y35Utf`57q(%Nx~yOhL>!LcUBRfT5i- zUfmu$NnY(+EuGYaBEC1k)V)tZk%r>W-8I`6m5{S(J6})$^p`D>?LHNTb;5XQ zgEx1VQ-@BCMB*P1_{c3hl?V{hOU;sPMEXl3iNqmtPJ{ADZO<}E=B3Em9T+b)-2B7< zId$x{0~ud--zgSF-p~^who46Q3k)XmdpBtt0#g@}@j8_ph>Vddi&g&}bP617yd>+S z*^YYtXvwQnCz9~2mQ0CsTa5q<^q?Tjw=pU2R-BF}j34YVBs`*uZY1F$S*KAHeDgjzTbIVF z*CVRemHgTuNn@^j;nEc(ug+UNL&B+MM>4*57m@Ogjw9i9-Aux@@_LelYvjGoCE>cs zr0gYM5~D?48YG$5Ie39}&U*OXLl%~tQRj{3{EUpS+>=kgfZI zjAP_A+sEW=a4T&bSsGbw@Zue39sLl0JUHplev+Kg%vvqkMx>v8nKYiS_er@}&yjGB zJ|kl@Y01CH*+O+%g}~H#n`z@TRmvixIo3Vk_#OA*W=X~w&AihHBE{`569Xgae33vnRwjL>fYj3{{;jCE)?xPr{{grIA&;tN*g2yt>qBYYKpv@w$3O0&C3OM!}es zD45zHZ9&doOUTUeIT;6Iz@>-_aSRJm#s*Tm*TE5y??IokW)y8Ts84ayA zR#%jIR2!=UHA^&I)=8^>h}N}hCCNJHoQ!kM$vWq(B6fAY=%{sHs>QR_ls~Sc`_6C;Yl$3iZC8qUDZ-+1T_h zF_-F)=H4D?`lY(Yh01T%4dz=l_rROu}Qpc8q30RH;F!~GX$6&MS8A?_>H%Y+joruCWm8SDFCMv70k z>D>!l6_g-hI8W@GGIab^4bRgYLw|cd3lVX=DOXQhPF61sD%n+TsjM!Fpv5}I{Cn11xfc`)=L6|=?tnr}FlMY|J)+6170K&hF1zX5A=EG+T zKbuJ2Uy&_L4{_Z4odSi>Vaq_@0JXK1OPTXE+n6~<94Dtz->8Oe#*g{}Lt-3J3q~Ds zC$|K|*_#dGSlMVVVWBsIaCss zRdAQ{t9jTDllAp16*L`mm735`{=v6n4cX@n{O~*cZ13*GAj|8vF4diYznYiG`owST z&i97!6vn8IGiAkudCl5+Bj>@S6AFR(I#TcKhD^>peiQ9h(j&*-zG_T!41`_ML<|%n z@p4_}vc-&=i<#i``-@C$YKPafaHoX-VDNu}#!-|QN*Kn=b@FTItgZ__?d4VJPO8|9 zX)6VPvY@iS>Q3?pqZ=}6&+-NvSomCUJBrx}foljh5P$jjhyfZQ{+Mcy^u8~npW5L% zj6lJ8dh?C{ZlL~S;~Mg_^Sa&7<@QHhL)YaUny1;DAekfB^s!-Lu2OqHRNoZd!vaN< z<9n=+$T&m(j2v)GA3Mo`wDK6C!8n2qL02`uq$_3PSG^m402$c|)-G~uVR4*l(Ro5}t3*(swc#VzH=+rX(j7bUiHv9ebHK&U-%IU3p}|x2 zWv%S-#{!NR;DV%St5NT$U&|r7gs#hh;X``;Eoilj7IGLfX86W3)=cJ+=-Tq<(mTVA z_e@@wQ=a{ZF#WTr4m!jOPVTPjQRVE?BtDr#)?{TQ6n{MQRhe9Fq1@Sh;U%B<^R^}4662L?b{o@q=C@Bd?x!j`m&8{ONVr8LJ;4@y9=dU z@`{gs)h2eSGDQhfo=P<+ChHck7ic~!xSgI`&<_2Tj`J4z)QrHbB9IzHjZ&$~!pVvW z*-B$rbE6sN9h^}|Z9*I;iJU(+Xcwi_B=yH+qx9z3kFY$%P@xO5=(BS}*n5R381)TT zL$c`xHg3NVlGvO-AYCxY9j@><@TLs$@%sH|@-B1}|M7zCH=E7o7rO_$q3)7g#EJVSu22>^`@8mM_M;_ zwPPmXhcV6msXq7o8~nh|zWM|Z5~!4rsYa6F(w59u??n*bl?f&S?+QfF!vy)FnwBie zE8NLq3n0_fSFdSRtmqDY@8HUq61{9OGZ?OJ?fZ9)6IO;{K1AFa<%{~|vmy)L=b;uE z6KHYbSTmo!-VsfMm%;91Hb}Q4v{xqc&f624QZ>I>TaoEX45Rgh{gf>VBz*ztg!@P3 oyg2@)(4Lvgw@cd3SO5S30Qrx0uK)l5 literal 0 HcmV?d00001 diff --git a/android/app/src/main/res/drawable-nodpi/orbit_britx_epg.webp b/android/app/src/main/res/drawable-nodpi/orbit_britx_epg.webp new file mode 100644 index 0000000000000000000000000000000000000000..d3970f32b92105d8dd74e4db09770839aa623363 GIT binary patch literal 6254 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 Y&Kje?{B9TrcrZ6)00000000000Gx$1vj6}9 literal 0 HcmV?d00001 diff --git a/android/app/src/main/res/drawable-nodpi/orbit_detour_bug.webp b/android/app/src/main/res/drawable-nodpi/orbit_detour_bug.webp new file mode 100644 index 0000000000000000000000000000000000000000..eaacd5d29ea2a78c88d5566d1b7f13a28ed77b36 GIT binary patch literal 4368 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 KVC^}u0001Uyne<2 literal 0 HcmV?d00001 diff --git a/android/app/src/main/res/drawable-nodpi/orbit_detour_epg.webp b/android/app/src/main/res/drawable-nodpi/orbit_detour_epg.webp new file mode 100644 index 0000000000000000000000000000000000000000..be75128a330e76b1a11796016fee51207b587199 GIT binary patch literal 8088 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 literal 0 HcmV?d00001 diff --git a/android/app/src/main/res/drawable-nodpi/orbit_junior_bug.webp b/android/app/src/main/res/drawable-nodpi/orbit_junior_bug.webp new file mode 100644 index 0000000000000000000000000000000000000000..70afd75e8c30a353b31225d7f6efdafb1fcbfa02 GIT binary patch literal 3482 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 literal 0 HcmV?d00001 diff --git a/android/app/src/main/res/drawable-nodpi/orbit_wild_bug.webp b/android/app/src/main/res/drawable-nodpi/orbit_wild_bug.webp new file mode 100644 index 0000000000000000000000000000000000000000..975131a3fa89bbfdaa24e7a0759a1cdda1f2b3b4 GIT binary patch literal 3160 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 literal 0 HcmV?d00001 diff --git a/android/app/src/main/res/drawable-nodpi/orbit_wild_epg.webp b/android/app/src/main/res/drawable-nodpi/orbit_wild_epg.webp new file mode 100644 index 0000000000000000000000000000000000000000..2d2c9bbb7455a977d1671190246432a857d9b9c9 GIT binary patch literal 5614 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 I05Bo|06%>oxBvhE literal 0 HcmV?d00001 diff --git a/android/app/src/test/java/tv/orbit/classic/ChannelPresetsTest.kt b/android/app/src/test/java/tv/orbit/classic/ChannelPresetsTest.kt index 81d261b..c8f6f9c 100644 --- a/android/app/src/test/java/tv/orbit/classic/ChannelPresetsTest.kt +++ b/android/app/src/test/java/tv/orbit/classic/ChannelPresetsTest.kt @@ -137,4 +137,22 @@ class ChannelPresetsTest { assertEquals(all.size, all.distinct().size) assertTrue(SmartChannels.first { it.number == 142 }.seriesCount >= all.size) } + + @Test fun detourAndBritXAreFilmChannelsWithTheirOwnRest() { + listOf(114, 115).forEach { number -> + val channel = SmartChannels.first { it.number == number } + assertNotNull(channel.filmRestMs) + assertFalse(channel.finishTogether) + assertEquals(ChannelCategory.Movies, ChannelBrands.forNumber(number)!!.category) + } + assertEquals(ChannelCategory.Kids, ChannelBrands.forNumber(125)!!.category) + assertEquals(ChannelCategory.Documentaries, ChannelBrands.forNumber(143)!!.category) + } + + @Test fun aFilmChannelsFallbackShowsFilmsNotEpisodes() { + val film = { id: String -> PoolSeries(id, "Film $id", listOf(PoolEpisode(id, "s$id", EpisodeMark(1, 1), "Film $id", 60_000))) } + val loop = loopFromPool(listOf(film("a"), film("b")), ChannelProgramming(114, "Detour", listOf("x")))!! + assertEquals(listOf("Movie", "Movie"), loop.lineup.map { it.item.type }) + assertEquals("Film a", loop.lineup.first().item.title) + } } diff --git a/android/app/src/test/java/tv/orbit/classic/WatchingControlsTest.kt b/android/app/src/test/java/tv/orbit/classic/WatchingControlsTest.kt index 195e76a..3efdf95 100644 --- a/android/app/src/test/java/tv/orbit/classic/WatchingControlsTest.kt +++ b/android/app/src/test/java/tv/orbit/classic/WatchingControlsTest.kt @@ -36,14 +36,6 @@ class WatchingControlsTest { assertEquals(2, typed.slot) } - @Test - fun subtitlesStepThroughTracksThenOff() { - assertEquals(0, nextSubtitle(null, 2)) - assertEquals(1, nextSubtitle(0, 2)) - assertNull(nextSubtitle(1, 2)) - assertNull(nextSubtitle(null, 0)) - } - @Test fun subtitlesKeepTheirLanguageAcrossProgrammes() { assertEquals(1, subtitleFor("eng", listOf("fre", "eng"))) @@ -51,4 +43,13 @@ class WatchingControlsTest { assertNull(subtitleFor(null, listOf("eng"))) assertNull(subtitleFor("eng", emptyList())) } + + @Test + fun tracksGetPlainNames() { + assertEquals("Director's commentary", trackLabel("Director's commentary", "eng", 0)) + assertEquals("English", trackLabel(null, "eng", 0)) + assertEquals("Japanese", trackLabel("", "ja", 1)) + assertEquals("Track 2", trackLabel(null, "und", 1)) + assertEquals("Track 1", trackLabel(null, null, 0)) + } } diff --git a/design/channels/build-more-brands.py b/design/channels/build-more-brands.py new file mode 100644 index 0000000..e7bc119 --- /dev/null +++ b/design/channels/build-more-brands.py @@ -0,0 +1,57 @@ +#!/usr/bin/env python3 +"""Vector logos for Detour, BritX, Orbit Junior and Orbit Wild, in the family style of the other channels. +Detour and BritX follow Paul's concept boards in docs/briefs/concepts. BritX's X is amber so it can't be mistaken for Orbit X.""" +import html, os, subprocess, sys +from pathlib import Path +from PIL import Image +HERE = Path(__file__).parent +ROOT = HERE.parents[1] +RES = ROOT / 'android/app/src/main/res/drawable-nodpi' +ENV = dict(os.environ) +if os.environ.get('ORBIT_FONT_HOME'): ENV['XDG_DATA_HOME'] = os.environ['ORBIT_FONT_HOME'] +BARLOW, ANTON, ROUND = 'Barlow', 'Anton', 'Comfortaa' +SILVER = '' +CRESCENT = 'M335 132C141 114 66 384 257 475C143 363 185 207 335 132Z' +def text(x, y, value, size, fill, family=BARLOW, weight='400', extra=''): + return f'{html.escape(value)}' +def mark(colour, sx=.26, dx=-5, dy=1): + return (f'' + f'') +def svg(w, h, body, defs=''): + return f'{SILVER}{defs}{body}' +ORBIT = text(134, 50, 'ORBIT', 18, '#C9D3E3', BARLOW, '600', 'letter-spacing="5"') +def write(name, kind, content, size): + folder = HERE / name.replace('_', '-'); folder.mkdir(exist_ok=True) + src = folder / f'{name}_{kind}.svg'; src.write_text(content) + png = folder / f'{name}_{kind}.png' + subprocess.run(['vectorcraft-cli', 'convert', str(src), str(png)], env=ENV, check=True, stdout=subprocess.DEVNULL) + Image.open(png).convert('RGBA').resize(size, Image.LANCZOS).save(RES / f'{name}_{kind}.webp', quality=92) + png.unlink() +def both(name, epg, bug, defs=''): + write(name, 'epg', svg(512, 160, epg, defs), (256, 80)) + write(name, 'bug', svg(384, 128, bug, defs), (144, 48)) + +arrow = lambda x, y, w, c: f'' +both('orbit_detour', + mark('#FF2D7E') + ORBIT + '' + text(150, 118, 'DETOU', 72, '#F5F1E8', ANTON) + text(312, 118, 'R', 72, '#FF2D7E', ANTON) + '' + arrow(136, 142, 196, '#FFD23F'), + mark('#FF2D7E', .2, -6, -3) + '' + text(118, 90, 'DETOU', 62, '#F5F1E8', ANTON) + text(257, 90, 'R', 62, '#FF2D7E', ANTON) + '' + arrow(104, 112, 170, '#FFD23F')) +both('orbit_britx', + mark('#E8A33D') + ORBIT + '' + text(150, 122, 'BRIT', 78, 'url(#slate)', ANTON) + text(274, 122, 'X', 78, '#E8A33D', ANTON) + '', + mark('#E8A33D', .2, -6, -3) + '' + text(118, 94, 'BRIT', 68, 'url(#slate)', ANTON) + text(226, 94, 'X', 68, '#E8A33D', ANTON) + '', + '') +letters = [('J', '#FF5A4F'), ('U', '#FFC933'), ('N', '#3FC1FF'), ('I', '#5BD46A'), ('O', '#FF8A3D'), ('R', '#C77DFF')] +def junior(x, y, size, step): + out, cx = '', x + for (c, f), w in zip(letters, (.72, .92, .95, .42, 1.0, .85)): + out += text(f'{cx:.0f}', y, c, size, f, ROUND, '700', f'stroke="{f}" stroke-width="3"') + cx += step * w + return out +both('orbit_junior', + mark('#FFC933') + ORBIT + junior(134, 112, 60, 50), + mark('#FFC933', .2, -6, -3) + junior(104, 86, 52, 43)) +def claws(x, y, h, c): + return ''.join(f'' for i in range(3)) +both('orbit_wild', + mark('#F28C28') + ORBIT + text(136, 122, 'WILD', 70, 'url(#earth)', ANTON) + claws(268, 66, 56, '#F28C28'), + mark('#F28C28', .2, -6, -3) + text(106, 94, 'WILD', 68, 'url(#earth)', ANTON) + claws(238, 42, 54, '#F28C28'), + '') diff --git a/design/channels/orbit-britx/orbit_britx_bug.svg b/design/channels/orbit-britx/orbit_britx_bug.svg new file mode 100644 index 0000000..f466b31 --- /dev/null +++ b/design/channels/orbit-britx/orbit_britx_bug.svg @@ -0,0 +1 @@ +BRITX \ No newline at end of file diff --git a/design/channels/orbit-britx/orbit_britx_epg.svg b/design/channels/orbit-britx/orbit_britx_epg.svg new file mode 100644 index 0000000..3d8b8db --- /dev/null +++ b/design/channels/orbit-britx/orbit_britx_epg.svg @@ -0,0 +1 @@ +ORBITBRITX \ No newline at end of file diff --git a/design/channels/orbit-detour/orbit_detour_bug.svg b/design/channels/orbit-detour/orbit_detour_bug.svg new file mode 100644 index 0000000..7dfaac1 --- /dev/null +++ b/design/channels/orbit-detour/orbit_detour_bug.svg @@ -0,0 +1 @@ +DETOUR \ No newline at end of file diff --git a/design/channels/orbit-detour/orbit_detour_epg.svg b/design/channels/orbit-detour/orbit_detour_epg.svg new file mode 100644 index 0000000..9e795c7 --- /dev/null +++ b/design/channels/orbit-detour/orbit_detour_epg.svg @@ -0,0 +1 @@ +ORBITDETOUR \ No newline at end of file diff --git a/design/channels/orbit-junior/orbit_junior_bug.svg b/design/channels/orbit-junior/orbit_junior_bug.svg new file mode 100644 index 0000000..196e2b7 --- /dev/null +++ b/design/channels/orbit-junior/orbit_junior_bug.svg @@ -0,0 +1 @@ +JUNIOR \ No newline at end of file diff --git a/design/channels/orbit-junior/orbit_junior_epg.svg b/design/channels/orbit-junior/orbit_junior_epg.svg new file mode 100644 index 0000000..14e4a6f --- /dev/null +++ b/design/channels/orbit-junior/orbit_junior_epg.svg @@ -0,0 +1 @@ +ORBITJUNIOR \ No newline at end of file diff --git a/design/channels/orbit-wild/orbit_wild_bug.svg b/design/channels/orbit-wild/orbit_wild_bug.svg new file mode 100644 index 0000000..d190de8 --- /dev/null +++ b/design/channels/orbit-wild/orbit_wild_bug.svg @@ -0,0 +1 @@ +WILD \ No newline at end of file diff --git a/design/channels/orbit-wild/orbit_wild_epg.svg b/design/channels/orbit-wild/orbit_wild_epg.svg new file mode 100644 index 0000000..36cf01f --- /dev/null +++ b/design/channels/orbit-wild/orbit_wild_epg.svg @@ -0,0 +1 @@ +ORBITWILD \ No newline at end of file diff --git a/docs/roadmap.md b/docs/roadmap.md index cac57df..8371daf 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -71,7 +71,7 @@ Audit and phased plan are in review, from `docs/briefs/indexing-programming-engi ## Smart channels status -Phase A of [the Smart Channels Foundation brief](briefs/smart-channels-foundation-2026-11-01.md) passed Paul's review on 2 November 2026, with Paradox on the box (VERIFICATION-0.39.0.md). Phase B (Terra and After Dark, VERIFICATION-0.40.0.md) and Phase C (Vortex, plus fewer repeats on Terra, VERIFICATION-0.41.0.md) are on the box as 0.41, tested and awaiting Paul's review. Day parts are on the box on Vortex and After Dark (VERIFICATION-0.42.0.md). Phase D (the remaining channels) needs separate approval. Reviewed suggestions are built and in use on After Dark and Vortex (VERIFICATION-0.47.0.md), the first piece of the programming director. Vortex Movie Night, the first weekday event, is on the box. Phase D has started with Orbit Gold (111), Heroes (121) and Evidence (141), on the box as 0.49, then ToonZ (122), Rewind (123) and Orbit X (124), all on the box as 0.51 with ten smart channels running (VERIFICATION-0.51.0.md). Orbit Three (112), Riot (113) and Dark Files (142, with day parts from the bible) are on the box as 0.59, making 13 smart channels (VERIFICATION-0.59.0.md). Still to add: Orbit 2, Wonder, PixelZ, Orbit VHS, Orbit Sci-Fi, Orbit Horror, Orbit Action, Orbit Romance, Orbit Premiere, Odyssey and Ring X. Numbers are placeholders until Paul maps the lineup. A show may run on several channels but never at the same moment. Animated idents are design files only and aren't in the app yet. +Phase A of [the Smart Channels Foundation brief](briefs/smart-channels-foundation-2026-11-01.md) passed Paul's review on 2 November 2026, with Paradox on the box (VERIFICATION-0.39.0.md). Phase B (Terra and After Dark, VERIFICATION-0.40.0.md) and Phase C (Vortex, plus fewer repeats on Terra, VERIFICATION-0.41.0.md) are on the box as 0.41, tested and awaiting Paul's review. Day parts are on the box on Vortex and After Dark (VERIFICATION-0.42.0.md). Phase D (the remaining channels) needs separate approval. Reviewed suggestions are built and in use on After Dark and Vortex (VERIFICATION-0.47.0.md), the first piece of the programming director. Vortex Movie Night, the first weekday event, is on the box. Phase D has started with Orbit Gold (111), Heroes (121) and Evidence (141), on the box as 0.49, then ToonZ (122), Rewind (123) and Orbit X (124), all on the box as 0.51 with ten smart channels running (VERIFICATION-0.51.0.md). Orbit Three (112), Riot (113) and Dark Files (142, with day parts from the bible) are on the box as 0.59, making 13 smart channels (VERIFICATION-0.59.0.md). Paul's own channels, Detour (114), BritX (115), Orbit Junior (125) and Orbit Wild (143), are built as 0.63 from docs/briefs/new-channel-ideas-2026-11-02.md; Orbit Unsolved is held, as Dark Files covers it. Still to add: Orbit 2, Wonder, PixelZ, Orbit VHS, Orbit Sci-Fi, Orbit Horror, Orbit Action, Orbit Romance, Orbit Premiere, Odyssey and Ring X. Numbers are placeholders until Paul maps the lineup. A show may run on several channels but never at the same moment. Animated idents are design files only and aren't in the app yet. ## Programming director