fix(live): left lands on the selected group, right enters a fresh group at the top

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-26 00:50:36 +02:00
parent d7da1b2ff4
commit c394d8e946
2 changed files with 46 additions and 7 deletions

View File

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