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:
@@ -12,8 +12,8 @@ android {
|
|||||||
applicationId = "dev.castarr.tv"
|
applicationId = "dev.castarr.tv"
|
||||||
minSdk = 26
|
minSdk = 26
|
||||||
targetSdk = 35
|
targetSdk = 35
|
||||||
versionCode = 13
|
versionCode = 14
|
||||||
versionName = "0.7.0"
|
versionName = "0.7.1"
|
||||||
}
|
}
|
||||||
|
|
||||||
// Release signing from environment (see ~/.keys/castarr-release.env on the
|
// Release signing from environment (see ~/.keys/castarr-release.env on the
|
||||||
|
|||||||
@@ -14,15 +14,21 @@ import androidx.compose.foundation.layout.padding
|
|||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
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.lazy.items
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
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.focus.onFocusChanged
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
@@ -76,6 +82,18 @@ fun LiveScreen(state: AppState) {
|
|||||||
return
|
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()) {
|
Row(Modifier.fillMaxSize()) {
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -87,10 +105,14 @@ fun LiveScreen(state: AppState) {
|
|||||||
verticalArrangement = Arrangement.spacedBy(2.dp),
|
verticalArrangement = Arrangement.spacedBy(2.dp),
|
||||||
) {
|
) {
|
||||||
item {
|
item {
|
||||||
|
val selected = !state.favoritesOnly && state.groupFilter == null
|
||||||
GroupItem(
|
GroupItem(
|
||||||
label = "Alle Sender",
|
label = "Alle Sender",
|
||||||
count = allChannels.size,
|
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.favoritesOnly = false
|
||||||
state.groupFilter = null
|
state.groupFilter = null
|
||||||
@@ -102,6 +124,9 @@ fun LiveScreen(state: AppState) {
|
|||||||
label = "★ Favoriten",
|
label = "★ Favoriten",
|
||||||
count = favorites.size,
|
count = favorites.size,
|
||||||
selected = state.favoritesOnly,
|
selected = state.favoritesOnly,
|
||||||
|
modifier = intoList.then(
|
||||||
|
if (state.favoritesOnly) Modifier.focusRequester(railFocus) else Modifier
|
||||||
|
),
|
||||||
) {
|
) {
|
||||||
state.favoritesOnly = true
|
state.favoritesOnly = true
|
||||||
state.groupFilter = null
|
state.groupFilter = null
|
||||||
@@ -122,6 +147,9 @@ fun LiveScreen(state: AppState) {
|
|||||||
label = group,
|
label = group,
|
||||||
count = remember(allChannels, group) { allChannels.count { it.group == group } },
|
count = remember(allChannels, group) { allChannels.count { it.group == group } },
|
||||||
selected = state.groupFilter == group,
|
selected = state.groupFilter == group,
|
||||||
|
modifier = intoList.then(
|
||||||
|
if (state.groupFilter == group) Modifier.focusRequester(railFocus) else Modifier
|
||||||
|
),
|
||||||
) {
|
) {
|
||||||
state.favoritesOnly = false
|
state.favoritesOnly = false
|
||||||
state.groupFilter = group
|
state.groupFilter = group
|
||||||
@@ -130,6 +158,7 @@ fun LiveScreen(state: AppState) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
|
state = listState,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.weight(1f)
|
.weight(1f)
|
||||||
.fillMaxHeight(),
|
.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(
|
ChannelRow(
|
||||||
channel = channel,
|
channel = channel,
|
||||||
// Stable channel number = position in "Alle Sender",
|
// Stable channel number = position in "Alle Sender",
|
||||||
// matching the remote's number pad in every view.
|
// matching the remote's number pad in every view.
|
||||||
number = allChannels.indexOf(channel) + 1,
|
number = allChannels.indexOf(channel) + 1,
|
||||||
|
modifier = Modifier
|
||||||
|
.focusProperties { left = railFocus }
|
||||||
|
.then(if (listIndex == 0) Modifier.focusRequester(listFocus) else Modifier),
|
||||||
showGroup = state.groupFilter == null,
|
showGroup = state.groupFilter == null,
|
||||||
nowNext = state.nowNext(channel),
|
nowNext = state.nowNext(channel),
|
||||||
playing = state.currentChannel?.url == channel.url,
|
playing = state.currentChannel?.url == channel.url,
|
||||||
@@ -175,12 +207,18 @@ fun LiveScreen(state: AppState) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@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(
|
Surface(
|
||||||
// Focusing a group opens it right away (browse like a menu);
|
// Focusing a group opens it right away (browse like a menu);
|
||||||
// clicking is not required, but harmless.
|
// clicking is not required, but harmless.
|
||||||
onClick = onSelect,
|
onClick = onSelect,
|
||||||
modifier = Modifier
|
modifier = modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.onFocusChanged { if (it.isFocused) onSelect() },
|
.onFocusChanged { if (it.isFocused) onSelect() },
|
||||||
shape = ClickableSurfaceDefaults.shape(RoundedCornerShape(10.dp)),
|
shape = ClickableSurfaceDefaults.shape(RoundedCornerShape(10.dp)),
|
||||||
@@ -218,6 +256,7 @@ private fun GroupItem(label: String, count: Int, selected: Boolean, onSelect: ()
|
|||||||
private fun ChannelRow(
|
private fun ChannelRow(
|
||||||
channel: Channel,
|
channel: Channel,
|
||||||
number: Int,
|
number: Int,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
showGroup: Boolean,
|
showGroup: Boolean,
|
||||||
nowNext: NowNext,
|
nowNext: NowNext,
|
||||||
playing: Boolean,
|
playing: Boolean,
|
||||||
@@ -229,7 +268,7 @@ private fun ChannelRow(
|
|||||||
Surface(
|
Surface(
|
||||||
onClick = onClick,
|
onClick = onClick,
|
||||||
onLongClick = onLongClick,
|
onLongClick = onLongClick,
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = modifier.fillMaxWidth(),
|
||||||
shape = ClickableSurfaceDefaults.shape(RoundedCornerShape(12.dp)),
|
shape = ClickableSurfaceDefaults.shape(RoundedCornerShape(12.dp)),
|
||||||
colors = ClickableSurfaceDefaults.colors(
|
colors = ClickableSurfaceDefaults.colors(
|
||||||
containerColor = if (playing) CastarrColors.accentDim else Color.Transparent,
|
containerColor = if (playing) CastarrColors.accentDim else Color.Transparent,
|
||||||
|
|||||||
Reference in New Issue
Block a user