fix(live): returning from playback keeps the group you were in
Some checks failed
Build TV app / build (push) Failing after 2s

The rail opens whatever gets focused, so when the player closed and focus
landed on "Alle Sender", the view silently switched away from the group
the viewer had been browsing. A restore flag now suppresses that
auto-select until focus has been placed on the channel that was watched.

Completes #13.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
be-nj
2026-08-26 02:20:17 +02:00
parent c0adce2b09
commit f157323626
3 changed files with 18 additions and 5 deletions

View File

@@ -12,8 +12,8 @@ android {
applicationId = "dev.castarr.tv" applicationId = "dev.castarr.tv"
minSdk = 26 minSdk = 26
targetSdk = 35 targetSdk = 35
versionCode = 23 versionCode = 24
versionName = "0.9.0" versionName = "0.9.1"
} }
// Release signing from environment (see ~/.keys/castarr-release.env on the // Release signing from environment (see ~/.keys/castarr-release.env on the

View File

@@ -51,6 +51,13 @@ class AppState(
/** Channel to restore focus to when the list comes back (#13). */ /** Channel to restore focus to when the list comes back (#13). */
var lastWatched by mutableStateOf<Channel?>(null) var lastWatched by mutableStateOf<Channel?>(null)
/**
* True while the list is being restored after playback. The group rail
* opens whatever gets focused, so without this the focus landing there
* would silently switch the view back to "Alle Sender".
*/
var restorePending by mutableStateOf(false)
private val reentryHandler = android.os.Handler(android.os.Looper.getMainLooper()) private val reentryHandler = android.os.Handler(android.os.Looper.getMainLooper())
private var lastStoppedUrl: String = "" private var lastStoppedUrl: String = ""
private var lastStoppedAt: Long = 0L private var lastStoppedAt: Long = 0L
@@ -190,6 +197,7 @@ class AppState(
} }
fun stopPlayback() { fun stopPlayback() {
restorePending = lastWatched != null
reentryHandler.removeCallbacksAndMessages(null) reentryHandler.removeCallbacksAndMessages(null)
lastStoppedUrl = currentChannel?.url.orEmpty() lastStoppedUrl = currentChannel?.url.orEmpty()
lastStoppedAt = System.currentTimeMillis() lastStoppedAt = System.currentTimeMillis()

View File

@@ -106,11 +106,12 @@ fun LiveScreen(state: AppState) {
// A fresh group starts at the top; a return from playback does not. // A fresh group starts at the top; a return from playback does not.
if (restoreIndex == null) listState.scrollToItem(0) if (restoreIndex == null) listState.scrollToItem(0)
} }
LaunchedEffect(restoreIndex, channels.size) { LaunchedEffect(restoreIndex, channels.size, state.restorePending) {
if (restoreIndex != null && !restored) { if (restoreIndex != null && (state.restorePending || !restored)) {
listState.scrollToItem(restoreIndex) listState.scrollToItem(restoreIndex)
runCatching { restoreFocus.requestFocus() } runCatching { restoreFocus.requestFocus() }
restored = true restored = true
state.restorePending = false
} }
} }
val intoList = if (channels.isEmpty()) Modifier val intoList = if (channels.isEmpty()) Modifier
@@ -135,6 +136,7 @@ fun LiveScreen(state: AppState) {
modifier = intoList.then( modifier = intoList.then(
if (selected) Modifier.focusRequester(railFocus) else Modifier if (selected) Modifier.focusRequester(railFocus) else Modifier
), ),
suppressAutoSelect = { state.restorePending },
) { ) {
state.favoritesOnly = false state.favoritesOnly = false
state.groupFilter = null state.groupFilter = null
@@ -149,6 +151,7 @@ fun LiveScreen(state: AppState) {
modifier = intoList.then( modifier = intoList.then(
if (state.favoritesOnly) Modifier.focusRequester(railFocus) else Modifier if (state.favoritesOnly) Modifier.focusRequester(railFocus) else Modifier
), ),
suppressAutoSelect = { state.restorePending },
) { ) {
state.favoritesOnly = true state.favoritesOnly = true
state.groupFilter = null state.groupFilter = null
@@ -172,6 +175,7 @@ fun LiveScreen(state: AppState) {
modifier = intoList.then( modifier = intoList.then(
if (state.groupFilter == group) Modifier.focusRequester(railFocus) else Modifier if (state.groupFilter == group) Modifier.focusRequester(railFocus) else Modifier
), ),
suppressAutoSelect = { state.restorePending },
) { ) {
state.favoritesOnly = false state.favoritesOnly = false
state.groupFilter = group state.groupFilter = group
@@ -237,6 +241,7 @@ private fun GroupItem(
count: Int, count: Int,
selected: Boolean, selected: Boolean,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
suppressAutoSelect: () -> Boolean = { false },
onSelect: () -> Unit, onSelect: () -> Unit,
) { ) {
Surface( Surface(
@@ -245,7 +250,7 @@ private fun GroupItem(
onClick = onSelect, onClick = onSelect,
modifier = modifier modifier = modifier
.fillMaxWidth() .fillMaxWidth()
.onFocusChanged { if (it.isFocused) onSelect() }, .onFocusChanged { if (it.isFocused && !suppressAutoSelect()) onSelect() },
shape = ClickableSurfaceDefaults.shape(RoundedCornerShape(10.dp)), shape = ClickableSurfaceDefaults.shape(RoundedCornerShape(10.dp)),
scale = ClickableSurfaceDefaults.scale(focusedScale = 1f), scale = ClickableSurfaceDefaults.scale(focusedScale = 1f),
colors = ClickableSurfaceDefaults.colors( colors = ClickableSurfaceDefaults.colors(