|
|
|
@@ -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)
|
|
|
|
|