From 8ea727fdf3ff98e641e4048a1dae45edfe36d8a5 Mon Sep 17 00:00:00 2001 From: Pauljd Date: Mon, 2 Nov 2026 09:08:44 +0000 Subject: [PATCH] Move smart channel buttons to their own scrolling screen (0.51.0) Co-Authored-By: Claude Opus 5.5 (1M context) --- android/app/build.gradle.kts | 4 +-- .../tv/orbit/classic/ChannelSuggestions.kt | 26 +++++++++++++++++++ .../main/java/tv/orbit/classic/EmbyClient.kt | 2 +- .../tv/orbit/classic/EmbyConnectionScreen.kt | 15 ++++++----- .../tv/orbit/classic/SecureMediaDataSource.kt | 2 +- 5 files changed, 38 insertions(+), 11 deletions(-) diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index 4c51cd4..0990da9 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 = 51 - versionName = "0.50.0-six-channels" + versionCode = 52 + versionName = "0.51.0-channel-list" } buildFeatures { compose = true } compileOptions { sourceCompatibility = JavaVersion.VERSION_17; targetCompatibility = JavaVersion.VERSION_17 } diff --git a/android/app/src/main/java/tv/orbit/classic/ChannelSuggestions.kt b/android/app/src/main/java/tv/orbit/classic/ChannelSuggestions.kt index 5e47a45..fea08e0 100644 --- a/android/app/src/main/java/tv/orbit/classic/ChannelSuggestions.kt +++ b/android/app/src/main/java/tv/orbit/classic/ChannelSuggestions.kt @@ -7,6 +7,7 @@ import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp @@ -118,3 +119,28 @@ internal fun SuggestionsScreen(programming: ChannelProgramming, suggestions: Lis } } } + +/** Every smart channel in one list that scrolls, however many there are. */ +@Composable +internal fun SmartChannelsScreen(busy: Boolean, message: String?, modifier: Modifier, isOn: (Int) -> Boolean, canStart: (ChannelProgramming) -> Boolean, + onSwitch: (ChannelProgramming, Boolean) -> Unit, onSuggestions: (ChannelProgramming) -> Unit, onBack: () -> Unit) { + BackHandler(onBack = onBack) + val firstFocus = androidx.compose.runtime.remember { androidx.compose.ui.focus.FocusRequester() } + androidx.compose.runtime.LaunchedEffect(Unit) { firstFocus.requestFocus() } + Column(modifier.fillMaxWidth().background(Navy).padding(14.dp), verticalArrangement = Arrangement.spacedBy(6.dp)) { + Text("Smart channels", color = Color.White, fontSize = 20.sp) + message?.let { Text(it, color = Yellow, fontSize = 13.sp, maxLines = 4) } + LazyColumn(Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(6.dp)) { + items(SmartChannels.sortedBy { it.number }, key = { it.number }) { programming -> + val on = isOn(programming.number) + Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(6.dp)) { + val switch = if (programming == SmartChannels.minByOrNull { it.number }) Modifier.focusRequester(firstFocus) else Modifier + OrbitButton("${programming.number} ${programming.name}: smart schedule is ${if (on) "ON. Press for its loop" else "OFF. Press to turn on"}", switch.weight(1f), + enabled = !busy && canStart(programming), onClick = { onSwitch(programming, !on) }) + if (programming.suggestionRules.isNotEmpty()) OrbitButton("Bigger pool: review", enabled = !busy && canStart(programming), onClick = { onSuggestions(programming) }) + } + } + } + OrbitButton("Back", onClick = onBack) + } +} 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 349471f..8e9a720 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.50.0\"") + connection.setRequestProperty("X-Emby-Authorization", "Emby Client=\"Orbit TV\", Device=\"Orbit TV\", DeviceId=\"$deviceId\", Version=\"0.51.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 3f77712..0f0436f 100644 --- a/android/app/src/main/java/tv/orbit/classic/EmbyConnectionScreen.kt +++ b/android/app/src/main/java/tv/orbit/classic/EmbyConnectionScreen.kt @@ -99,6 +99,7 @@ internal fun EmbyConnectionScreen(modifier: Modifier = Modifier, visible: Boolea var scheduledChannels by remember { mutableStateOf>(emptyMap()) } var suggesting by remember { mutableStateOf(null) } var suggestions by remember { mutableStateOf>(emptyList()) } + var managingChannels by remember { mutableStateOf(false) } var savingSuggestions by remember { mutableStateOf(false) } var tickedSuggestions by remember { mutableStateOf>(emptySet()) } var lastSmartPress by remember { mutableLongStateOf(0L) } @@ -561,6 +562,12 @@ internal fun EmbyConnectionScreen(modifier: Modifier = Modifier, visible: Boolea onBack = { if (!savingSuggestions) { job?.cancel(); busy = false }; suggesting = null }) return } + if (managingChannels) { + SmartChannelsScreen(busy, message, modifier, isOn = { scheduledChannels.containsKey(it) }, + canStart = { smartLoop(it) != null || ChannelPresets.any { preset -> preset.number == it.number } }, + onSwitch = { programming, on -> setSmartSchedule(programming, on) }, onSuggestions = { openSuggestions(it) }, onBack = { managingChannels = false }) + return + } if (reviewingSeries) { SeriesReviewScreen(seriesMatches.orEmpty(), approvedSeries, episodeSamples, busy, message, modifier, onToggle = { id -> @@ -698,6 +705,7 @@ internal fun EmbyConnectionScreen(modifier: Modifier = Modifier, visible: Boolea message?.let { Text(it, color = Yellow, fontSize = 14.sp) } Text("${result.items.size} records received${result.total?.let { " • $it available to this account" }.orEmpty()}", color = Color.White, fontSize = 16.sp) Text("Select a title, or create one channel from up to three playable titles.", color = Muted, fontSize = 13.sp) + OrbitButton("Smart channels", Modifier.fillMaxWidth(), enabled = !busy && session != null, onClick = { message = null; managingChannels = true }) OrbitButton(if (channel == null) "Create sample channel" else "Open TV Guide", Modifier.fillMaxWidth(), enabled = !busy && session != null, onClick = { if (channel != null) { selectedNumber = 101; onGuide() } else { @@ -727,13 +735,6 @@ internal fun EmbyConnectionScreen(modifier: Modifier = Modifier, visible: Boolea OrbitButton("${preset.number} ${preset.name} — ${if (built) "rebuild" else "build"}", Modifier.fillMaxWidth(), enabled = !busy && session != null, onClick = { buildPreset(preset) }) } - SmartChannels.forEach { programming -> - val on = scheduledChannels.containsKey(programming.number) - OrbitButton("${programming.number} ${programming.name} — smart schedule is ${if (on) "ON. Press to go back to its loop" else "OFF. Press to turn on"}", Modifier.fillMaxWidth(), - enabled = !busy && session != null && (smartLoop(programming) != null || ChannelPresets.any { it.number == programming.number }), onClick = { setSmartSchedule(programming, !on) }) - if (programming.suggestionRules.isNotEmpty()) OrbitButton("${programming.number} ${programming.name} — bigger pool: review suggestions", Modifier.fillMaxWidth(), - enabled = !busy && session != null && smartLoop(programming) != null, onClick = { openSuggestions(programming) }) - } OrbitButton("Check metadata coverage", Modifier.fillMaxWidth(), enabled = !busy && session != null, onClick = { message = null; reviewingCoverage = true }) OrbitButton("Library import test", Modifier.fillMaxWidth(), 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 38d9bb4..4d91e7b 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.50.0") + next.setRequestProperty("User-Agent", "OrbitTV/0.51.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()) {