1 Commits

Author SHA1 Message Date
be-nj
f157323626 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>
2026-08-26 02:20:17 +02:00
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(