0.81.2: right from a Settings section goes to its first switch
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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.81.1\"")
|
||||
connection.setRequestProperty("X-Emby-Authorization", "Emby Client=\"Orbit TV\", Device=\"Orbit TV\", DeviceId=\"$deviceId\", Version=\"0.81.2\"")
|
||||
request.token?.let { connection.setRequestProperty("X-Emby-Token", it) }
|
||||
if (request.method == "POST") {
|
||||
val bytes = request.body.orEmpty().toByteArray(Charsets.UTF_8)
|
||||
|
||||
@@ -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.81.1")
|
||||
next.setRequestProperty("User-Agent", "OrbitTV/0.81.2")
|
||||
next.setRequestProperty("Accept-Encoding", "identity")
|
||||
session.mediaHeaders(address).forEach { (key, value) -> next.setRequestProperty(key, value) }
|
||||
if (dataSpec.position != 0L || dataSpec.length != C.LENGTH_UNSET.toLong()) {
|
||||
|
||||
@@ -6,7 +6,9 @@ import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
@@ -16,6 +18,11 @@ import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.focus.onFocusChanged
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.input.key.Key
|
||||
import androidx.compose.ui.input.key.KeyEventType
|
||||
import androidx.compose.ui.input.key.key
|
||||
import androidx.compose.ui.input.key.onPreviewKeyEvent
|
||||
import androidx.compose.ui.input.key.type
|
||||
import androidx.compose.ui.graphics.ColorFilter
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.painterResource
|
||||
@@ -24,6 +31,7 @@ import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.tv.material3.Text
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
/** Choices about how Orbit behaves on this box. Nothing here is private, so it needs no encrypted storage. */
|
||||
internal class OrbitSettings(context: Context) {
|
||||
@@ -68,9 +76,12 @@ private val RailCorner = RoundedCornerShape(6.dp)
|
||||
|
||||
/** Moving onto a section shows it straight away, and right steps across into it. */
|
||||
@Composable
|
||||
private fun RailItem(section: SettingsSection, selected: Boolean, modifier: Modifier, onSelect: () -> Unit) {
|
||||
private fun RailItem(section: SettingsSection, selected: Boolean, modifier: Modifier, onRight: () -> Unit, onSelect: () -> Unit) {
|
||||
var focused by remember { mutableStateOf(false) }
|
||||
Row(modifier.fillMaxWidth().height(46.dp).onFocusChanged { focused = it.isFocused; if (it.isFocused) onSelect() }.clip(RailCorner)
|
||||
Row(modifier.fillMaxWidth().height(46.dp).onPreviewKeyEvent { event ->
|
||||
// Straight to the section's first switch. Left alone, right could land on the top bar instead.
|
||||
if (event.key == Key.DirectionRight) { if (event.type == KeyEventType.KeyDown) onRight(); true } else false
|
||||
}.onFocusChanged { focused = it.isFocused; if (it.isFocused) onSelect() }.clip(RailCorner)
|
||||
.background(if (focused) Yellow else if (selected) Blue else Panel).clickable(onClick = onSelect).padding(horizontal = 10.dp),
|
||||
verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(10.dp)) {
|
||||
Image(painterResource(section.icon), null, Modifier.size(26.dp), colorFilter = if (focused) ColorFilter.tint(Navy) else null)
|
||||
@@ -92,13 +103,13 @@ private fun SettingLine(title: String, detail: String, button: @Composable () ->
|
||||
|
||||
@Composable
|
||||
private fun ChannelsDetail(busy: Boolean, message: String?, isOn: (Int) -> Boolean,
|
||||
onSwitch: (ChannelProgramming, Boolean) -> Unit, onSuggestions: (ChannelProgramming) -> Unit, modifier: Modifier) {
|
||||
onSwitch: (ChannelProgramming, Boolean) -> Unit, onSuggestions: (ChannelProgramming) -> Unit, firstSwitch: FocusRequester, list: LazyListState, modifier: Modifier) {
|
||||
val all = SmartChannels.sortedBy { it.number }
|
||||
Column(modifier, verticalArrangement = Arrangement.spacedBy(6.dp)) {
|
||||
Text("${all.count { isOn(it.number) }} of ${all.size} channels on", color = Color.White, fontSize = 18.sp, fontWeight = FontWeight.SemiBold)
|
||||
Text("A channel that's on runs its full schedule. Once it's been on, turning it off leaves a short repeat.", color = Muted, fontSize = 12.sp, maxLines = 1, overflow = TextOverflow.Ellipsis)
|
||||
message?.let { Text(it, color = Yellow, fontSize = 13.sp, maxLines = 3, overflow = TextOverflow.Ellipsis) }
|
||||
LazyColumn(Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(4.dp)) {
|
||||
LazyColumn(Modifier.weight(1f), state = list, verticalArrangement = Arrangement.spacedBy(4.dp)) {
|
||||
items(all, key = { it.number }) { programming ->
|
||||
val on = isOn(programming.number)
|
||||
val brand = ChannelBrands.forNumber(programming.number)
|
||||
@@ -110,7 +121,7 @@ private fun ChannelsDetail(busy: Boolean, message: String?, isOn: (Int) -> Boole
|
||||
if (brand != null) Image(painterResource(brand.logo), brand.displayName, Modifier.width(88.dp).height(28.dp))
|
||||
Text(brand?.displayName ?: programming.name, Modifier.weight(1f), color = Color.White, fontSize = 14.sp, maxLines = 1, overflow = TextOverflow.Ellipsis)
|
||||
if (programming.suggestionRules.isNotEmpty()) OrbitButton("Titles", enabled = !busy, onClick = { onSuggestions(programming) })
|
||||
OrbitButton(if (on) "On" else "Off", Modifier.width(64.dp), selected = on, enabled = !busy, onClick = { onSwitch(programming, !on) })
|
||||
OrbitButton(if (on) "On" else "Off", Modifier.width(64.dp).then(if (programming == all.first()) Modifier.focusRequester(firstSwitch) else Modifier), selected = on, enabled = !busy, onClick = { onSwitch(programming, !on) })
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -119,7 +130,7 @@ private fun ChannelsDetail(busy: Boolean, message: String?, isOn: (Int) -> Boole
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ViewingDetail(totsOn: Boolean, windingDown: Boolean, onWindDown: (Boolean) -> Unit, modifier: Modifier) {
|
||||
private fun ViewingDetail(totsOn: Boolean, windingDown: Boolean, onWindDown: (Boolean) -> Unit, firstSwitch: FocusRequester, modifier: Modifier) {
|
||||
val context = LocalContext.current
|
||||
val settings = remember { OrbitSettings(context) }
|
||||
var subtitlesOn by remember { mutableStateOf(settings.subtitlesOn) }
|
||||
@@ -129,7 +140,7 @@ private fun ViewingDetail(totsOn: Boolean, windingDown: Boolean, onWindDown: (Bo
|
||||
var confirming by remember { mutableStateOf(false) }
|
||||
Column(modifier, verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
SettingLine("Subtitles", "Turn on English subtitles whenever you tune in, if the programme has them. You can still change them while watching, by pressing right.") {
|
||||
OrbitButton(if (subtitlesOn) "On" else "Off", Modifier.width(64.dp), selected = subtitlesOn, onClick = { subtitlesOn = !subtitlesOn; settings.subtitlesOn = subtitlesOn })
|
||||
OrbitButton(if (subtitlesOn) "On" else "Off", Modifier.width(64.dp).focusRequester(firstSwitch), selected = subtitlesOn, onClick = { subtitlesOn = !subtitlesOn; settings.subtitlesOn = subtitlesOn })
|
||||
}
|
||||
SettingLine("Channel logo", "Show the channel's small logo in the corner during programmes.") {
|
||||
OrbitButton(if (channelLogo) "On" else "Off", Modifier.width(64.dp), selected = channelLogo, onClick = { channelLogo = !channelLogo; settings.channelLogo = channelLogo })
|
||||
@@ -156,7 +167,7 @@ private fun ViewingDetail(totsOn: Boolean, windingDown: Boolean, onWindDown: (Bo
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun AccountDetail(server: String, titles: Long?, onSignOut: () -> Unit, modifier: Modifier) {
|
||||
private fun AccountDetail(server: String, titles: Long?, onSignOut: () -> Unit, firstSwitch: FocusRequester, modifier: Modifier) {
|
||||
val context = LocalContext.current
|
||||
val version = remember { runCatching { context.packageManager.getPackageInfo(context.packageName, 0).versionName }.getOrNull().orEmpty() }
|
||||
var confirming by remember { mutableStateOf(false) }
|
||||
@@ -172,7 +183,7 @@ private fun AccountDetail(server: String, titles: Long?, onSignOut: () -> Unit,
|
||||
OrbitButton("Sign out", onClick = onSignOut)
|
||||
}
|
||||
}
|
||||
} else SettingLine("Sign out", "Disconnect this box from Emby.") { OrbitButton("Sign out", onClick = { confirming = true }) }
|
||||
} else SettingLine("Sign out", "Disconnect this box from Emby.") { OrbitButton("Sign out", Modifier.focusRequester(firstSwitch), onClick = { confirming = true }) }
|
||||
Text("Orbit TV $version", color = Muted, fontSize = 12.sp)
|
||||
Text("Snake graphics by Bas de Reuver, bdrgames.nl", color = Muted, fontSize = 12.sp)
|
||||
}
|
||||
@@ -184,20 +195,26 @@ internal fun SettingsScreen(section: SettingsSection, onSection: (SettingsSectio
|
||||
isOn: (Int) -> Boolean, onSwitch: (ChannelProgramming, Boolean) -> Unit, onSuggestions: (ChannelProgramming) -> Unit, onSignOut: () -> Unit,
|
||||
windingDown: Boolean, onWindDown: (Boolean) -> Unit) {
|
||||
val firstFocus = remember { FocusRequester() }
|
||||
val firstSwitch = remember { FocusRequester() }
|
||||
val channelList = rememberLazyListState()
|
||||
val scope = rememberCoroutineScope()
|
||||
LaunchedEffect(Unit) { firstFocus.requestFocus() }
|
||||
Column(modifier.fillMaxWidth(), verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
Text("Settings", color = Color.White, fontSize = 22.sp, fontWeight = FontWeight.SemiBold)
|
||||
Row(Modifier.fillMaxWidth().weight(1f), horizontalArrangement = Arrangement.spacedBy(14.dp)) {
|
||||
Column(Modifier.width(176.dp), verticalArrangement = Arrangement.spacedBy(6.dp)) {
|
||||
SettingsSection.entries.forEach { item ->
|
||||
RailItem(item, item == section, if (item == section) Modifier.focusRequester(firstFocus) else Modifier) { onSection(item) }
|
||||
RailItem(item, item == section, if (item == section) Modifier.focusRequester(firstFocus) else Modifier, onRight = {
|
||||
// The channel list may have been scrolled, taking its first switch off the screen.
|
||||
scope.launch { if (item == SettingsSection.Channels) channelList.scrollToItem(0); runCatching { firstSwitch.requestFocus() } }
|
||||
}) { onSection(item) }
|
||||
}
|
||||
}
|
||||
val detail = Modifier.weight(1f).fillMaxHeight()
|
||||
when (section) {
|
||||
SettingsSection.Channels -> ChannelsDetail(busy, message, isOn, onSwitch, onSuggestions, detail)
|
||||
SettingsSection.Viewing -> ViewingDetail(isOn(TOTS_NUMBER), windingDown, onWindDown, detail)
|
||||
SettingsSection.Account -> AccountDetail(server, titles, onSignOut, detail)
|
||||
SettingsSection.Channels -> ChannelsDetail(busy, message, isOn, onSwitch, onSuggestions, firstSwitch, channelList, detail)
|
||||
SettingsSection.Viewing -> ViewingDetail(isOn(TOTS_NUMBER), windingDown, onWindDown, firstSwitch, detail)
|
||||
SettingsSection.Account -> AccountDetail(server, titles, onSignOut, firstSwitch, detail)
|
||||
}
|
||||
}
|
||||
Text("▲▼ Move ► Into this section OK Select Back Main menu", color = Muted, fontSize = 11.sp)
|
||||
|
||||
Reference in New Issue
Block a user