From deca5bd0cbba09cf08d8895538eba516d920ddf3 Mon Sep 17 00:00:00 2001 From: be-nj Date: Wed, 26 Aug 2026 00:50:36 +0200 Subject: [PATCH] fix(live): left lands on the selected group, right enters a fresh group at the top Co-Authored-By: Claude Fable 5 --- app/build.gradle.kts | 4 +- .../main/java/dev/castarr/tv/ui/LiveScreen.kt | 49 +++++++++++++++++-- 2 files changed, 46 insertions(+), 7 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index a673c17..28a4f63 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -12,8 +12,8 @@ android { applicationId = "dev.castarr.tv" minSdk = 26 targetSdk = 35 - versionCode = 13 - versionName = "0.7.0" + versionCode = 14 + versionName = "0.7.1" } // Release signing from environment (see ~/.keys/castarr-release.env on the diff --git a/app/src/main/java/dev/castarr/tv/ui/LiveScreen.kt b/app/src/main/java/dev/castarr/tv/ui/LiveScreen.kt index 131a617..08dab55 100644 --- a/app/src/main/java/dev/castarr/tv/ui/LiveScreen.kt +++ b/app/src/main/java/dev/castarr/tv/ui/LiveScreen.kt @@ -14,15 +14,21 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.lazy.items import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusProperties +import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.focus.onFocusChanged import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.font.FontWeight @@ -76,6 +82,18 @@ fun LiveScreen(state: AppState) { return } + // D-pad choreography: left from any channel row lands on the SELECTED + // group (not the spatially nearest one), right from the rail enters the + // list at the top — which also gets scrolled up on every group change. + val railFocus = remember { FocusRequester() } + val listFocus = remember { FocusRequester() } + val listState = rememberLazyListState() + LaunchedEffect(state.groupFilter, state.favoritesOnly) { + listState.scrollToItem(0) + } + val intoList = if (channels.isEmpty()) Modifier + else Modifier.focusProperties { right = listFocus } + Row(Modifier.fillMaxSize()) { LazyColumn( modifier = Modifier @@ -87,10 +105,14 @@ fun LiveScreen(state: AppState) { verticalArrangement = Arrangement.spacedBy(2.dp), ) { item { + val selected = !state.favoritesOnly && state.groupFilter == null GroupItem( label = "Alle Sender", count = allChannels.size, - selected = !state.favoritesOnly && state.groupFilter == null, + selected = selected, + modifier = intoList.then( + if (selected) Modifier.focusRequester(railFocus) else Modifier + ), ) { state.favoritesOnly = false state.groupFilter = null @@ -102,6 +124,9 @@ fun LiveScreen(state: AppState) { label = "★ Favoriten", count = favorites.size, selected = state.favoritesOnly, + modifier = intoList.then( + if (state.favoritesOnly) Modifier.focusRequester(railFocus) else Modifier + ), ) { state.favoritesOnly = true state.groupFilter = null @@ -122,6 +147,9 @@ fun LiveScreen(state: AppState) { label = group, count = remember(allChannels, group) { allChannels.count { it.group == group } }, selected = state.groupFilter == group, + modifier = intoList.then( + if (state.groupFilter == group) Modifier.focusRequester(railFocus) else Modifier + ), ) { state.favoritesOnly = false state.groupFilter = group @@ -130,6 +158,7 @@ fun LiveScreen(state: AppState) { } LazyColumn( + state = listState, modifier = Modifier .weight(1f) .fillMaxHeight(), @@ -151,12 +180,15 @@ fun LiveScreen(state: AppState) { ) } } - items(channels, key = { it.url + it.name }) { channel -> + itemsIndexed(channels, key = { _, c -> c.url + c.name }) { listIndex, channel -> ChannelRow( channel = channel, // Stable channel number = position in "Alle Sender", // matching the remote's number pad in every view. number = allChannels.indexOf(channel) + 1, + modifier = Modifier + .focusProperties { left = railFocus } + .then(if (listIndex == 0) Modifier.focusRequester(listFocus) else Modifier), showGroup = state.groupFilter == null, nowNext = state.nowNext(channel), playing = state.currentChannel?.url == channel.url, @@ -175,12 +207,18 @@ fun LiveScreen(state: AppState) { } @Composable -private fun GroupItem(label: String, count: Int, selected: Boolean, onSelect: () -> Unit) { +private fun GroupItem( + label: String, + count: Int, + selected: Boolean, + modifier: Modifier = Modifier, + onSelect: () -> Unit, +) { Surface( // Focusing a group opens it right away (browse like a menu); // clicking is not required, but harmless. onClick = onSelect, - modifier = Modifier + modifier = modifier .fillMaxWidth() .onFocusChanged { if (it.isFocused) onSelect() }, shape = ClickableSurfaceDefaults.shape(RoundedCornerShape(10.dp)), @@ -218,6 +256,7 @@ private fun GroupItem(label: String, count: Int, selected: Boolean, onSelect: () private fun ChannelRow( channel: Channel, number: Int, + modifier: Modifier = Modifier, showGroup: Boolean, nowNext: NowNext, playing: Boolean, @@ -229,7 +268,7 @@ private fun ChannelRow( Surface( onClick = onClick, onLongClick = onLongClick, - modifier = Modifier.fillMaxWidth(), + modifier = modifier.fillMaxWidth(), shape = ClickableSurfaceDefaults.shape(RoundedCornerShape(12.dp)), colors = ClickableSurfaceDefaults.colors( containerColor = if (playing) CastarrColors.accentDim else Color.Transparent,