From 244a8c97eda83f6dbb73591e2b1951592c5d5477 Mon Sep 17 00:00:00 2001 From: Jos Vooges | STH Date: Wed, 23 Sep 2026 01:43:20 +0200 Subject: [PATCH] Fix Events crash: bind FocusRequester before TopNav down to hub rail. --- apps/android-tv/app/build.gradle.kts | 4 +-- .../vonas/mediacluster/tv/ui/EventsScreen.kt | 36 +++++++++++++++---- 2 files changed, 32 insertions(+), 8 deletions(-) diff --git a/apps/android-tv/app/build.gradle.kts b/apps/android-tv/app/build.gradle.kts index d05422e..b8e91da 100644 --- a/apps/android-tv/app/build.gradle.kts +++ b/apps/android-tv/app/build.gradle.kts @@ -20,8 +20,8 @@ android { applicationId = "nl.vonas.mediacluster.tv" minSdk = 24 targetSdk = 36 - versionCode = 144 - versionName = "0.14.81" + versionCode = 145 + versionName = "0.14.82" buildConfigField("String", "DEFAULT_API_BASE", "\"https://master.vonas.nl\"") ndk { abiFilters += listOf("arm64-v8a", "armeabi-v7a") diff --git a/apps/android-tv/app/src/main/java/nl/vonas/mediacluster/tv/ui/EventsScreen.kt b/apps/android-tv/app/src/main/java/nl/vonas/mediacluster/tv/ui/EventsScreen.kt index 980085b..79b9f83 100644 --- a/apps/android-tv/app/src/main/java/nl/vonas/mediacluster/tv/ui/EventsScreen.kt +++ b/apps/android-tv/app/src/main/java/nl/vonas/mediacluster/tv/ui/EventsScreen.kt @@ -133,6 +133,7 @@ fun EventsScreen( var providerFilter by remember { mutableStateOf(null) } var nowMs by remember { mutableLongStateOf(System.currentTimeMillis()) } var filterBarFocusReady by remember { mutableStateOf(false) } + var hubRailFocusReady by remember { mutableStateOf(false) } /** Owner-id van first-rail FocusRequester — voorkomt dispose-race live↔upcoming. */ var railFocusOwner by remember { mutableStateOf(null) } val railFocusReady = railFocusOwner != null @@ -141,6 +142,7 @@ fun EventsScreen( val listState = rememberLazyListState() val firstRailFocus = remember { FocusRequester() } val restoreCardFocus = remember { FocusRequester() } + val hubRailFocus = remember { FocusRequester() } suspend fun refresh() { runCatching { api.listScheduleEvents() } @@ -253,7 +255,7 @@ fun EventsScreen( val filterBarFocus = remember { FocusRequester() } val firstRailUpTarget = when { showFilterBar && filterBarFocusReady -> filterBarFocus - hasHubRail -> contentFocusRequester + hasHubRail && hubRailFocusReady -> hubRailFocus else -> navFocusRequester } val filterFocusDown: FocusRequester? = when { @@ -342,19 +344,24 @@ fun EventsScreen( } val entryReady = when { - hasHubRail -> true + hasHubRail -> hubRailFocusReady showFilterBar -> filterBarFocusReady else -> railFocusReady } val needContentSink = when { loading && events.isEmpty() -> true error != null && events.isEmpty() -> true - hasHubRail -> false + // contentFocusRequester mag nooit "los" staan (TopNav.down → crash) + hasHubRail -> !hubRailFocusReady showFilterBar -> !filterBarFocusReady !hasRails -> true else -> !railFocusReady } + LaunchedEffect(hasHubRail) { + if (!hasHubRail) hubRailFocusReady = false + } + // Detail → back: card restore; fallback = chips of eerste rail. LaunchedEffect( pendingFocusRestore, @@ -374,7 +381,7 @@ fun EventsScreen( if (restoredSuccessfully) return val entryOk = awaitReady({ when { - hasHubRail -> true + hasHubRail -> hubRailFocusReady showFilterBar -> filterBarFocusReady else -> railFocusReady } @@ -499,12 +506,17 @@ fun EventsScreen( ) { if (hasHubRail) { item(key = "hub_rail") { + // Zolang TopNav-sink contentFocusRequester vasthoudt → hubRailFocus op tile. + // Zodra ready → contentFocusRequester op eerste tile. + val hubEntryFocus = + if (hubRailFocusReady) contentFocusRequester else hubRailFocus EventsHubRail( hubs = hubs, - focusRequester = contentFocusRequester, + focusRequester = hubEntryFocus, focusUp = navFocusRequester, onOpenF1 = onOpenF1Hub, onOpenViaplay = onOpenViaplayHub, + onFocusReady = { hubRailFocusReady = it }, onFocus = onContentFocused, ) } @@ -523,7 +535,11 @@ fun EventsScreen( liveCount = live.size, totalCount = filtered.size, firstFocusRequester = if (hasHubRail) filterBarFocus else contentFocusRequester, - focusUp = if (hasHubRail) contentFocusRequester else navFocusRequester, + focusUp = when { + hasHubRail && hubRailFocusReady -> contentFocusRequester + hasHubRail -> null + else -> navFocusRequester + }, focusDown = filterFocusDown, onFocusReady = { filterBarFocusReady = it }, onFocus = onContentFocused, @@ -1108,6 +1124,7 @@ private fun EventsHubRail( focusUp: FocusRequester?, onOpenF1: (() -> Unit)?, onOpenViaplay: (() -> Unit)?, + onFocusReady: ((Boolean) -> Unit)? = null, onFocus: () -> Unit, ) { LazyRow( @@ -1140,6 +1157,7 @@ private fun EventsHubRail( accent = accent, focusRequester = if (index == 0) focusRequester else null, focusUp = focusUp, + onFocusReady = if (index == 0) onFocusReady else null, onOpen = onOpen, onFocus = onFocus, ) @@ -1156,9 +1174,15 @@ private fun HubRailTile( accent: Color, focusRequester: FocusRequester?, focusUp: FocusRequester?, + onFocusReady: ((Boolean) -> Unit)? = null, onOpen: () -> Unit, onFocus: () -> Unit, ) { + // Ready = eerste tile is in composition (niet gekoppeld aan welk FocusRequester-object) + DisposableEffect(Unit) { + onFocusReady?.invoke(true) + onDispose { onFocusReady?.invoke(false) } + } var focused by remember { mutableStateOf(false) } Surface( onClick = onOpen,