From 406d0f4ed8a4cf36bab87df337d3e3be9c6bbc15 Mon Sep 17 00:00:00 2001 From: Barnabas Balogh Date: Sat, 9 May 2026 09:03:07 +0200 Subject: [PATCH] feat(tvseries): improve focus management for season and episode selection --- .../ui/screen/series/TvSeriesScreen.kt | 25 ++++++++++++------- .../series/components/TvSeriesComponents.kt | 10 +++++--- 2 files changed, 22 insertions(+), 13 deletions(-) diff --git a/app-tv/src/main/java/hu/bbara/purefin/ui/screen/series/TvSeriesScreen.kt b/app-tv/src/main/java/hu/bbara/purefin/ui/screen/series/TvSeriesScreen.kt index 01f08099..426aa8e3 100644 --- a/app-tv/src/main/java/hu/bbara/purefin/ui/screen/series/TvSeriesScreen.kt +++ b/app-tv/src/main/java/hu/bbara/purefin/ui/screen/series/TvSeriesScreen.kt @@ -77,20 +77,22 @@ internal fun TvSeriesScreenContent( mutableStateOf(defaultSeason.id) } val selectedSeason = series.seasons.firstOrNull { it.id == selectedSeasonId } ?: defaultSeason - val initialFocusedEpisodeId = remember(series.id, focusedSeasonId, focusedEpisodeId) { - val focusedEpisode = defaultSeason.episodes.firstOrNull { it.id == focusedEpisodeId } - focusedEpisode?.id ?: defaultSeason.nextUpEpisode()?.id - } - val firstContentFocusRequester = remember { FocusRequester() } + val initialFocusSeasonId = remember(series.id, focusedSeasonId) { defaultSeason.id } + val initialFocusSeason = series.seasons.firstOrNull { it.id == initialFocusSeasonId } ?: defaultSeason + val initialFocusedEpisodeId = initialFocusSeason.focusTargetEpisodeId(focusedEpisodeId) + val seasonTabFocusRequester = remember { FocusRequester() } + val waitingForInitialEpisodes = initialFocusedEpisodeId == null && + initialFocusSeason.episodes.isEmpty() && + initialFocusSeason.episodeCount > 0 LaunchedEffect(series.id, selectedSeason.id) { onLoadSeasonEpisodes(series.id, selectedSeason.id) } - LaunchedEffect(series.id, initialFocusedEpisodeId) { - if (initialFocusedEpisodeId != null) return@LaunchedEffect + LaunchedEffect(series.id, initialFocusedEpisodeId, waitingForInitialEpisodes) { + if (initialFocusedEpisodeId != null || waitingForInitialEpisodes) return@LaunchedEffect withFrameNanos { } - firstContentFocusRequester.requestFocus() + seasonTabFocusRequester.requestFocus() } TvMediaDetailScaffold( @@ -115,7 +117,7 @@ internal fun TvSeriesScreenContent( TvSeasonTabs( seasons = series.seasons, selectedSeason = selectedSeason, - firstItemFocusRequester = firstContentFocusRequester, + selectedItemFocusRequester = seasonTabFocusRequester, firstItemTestTag = SeriesFirstSeasonTabTag, onSelect = { selectedSeasonId = it.id }, modifier = Modifier.fillMaxWidth() @@ -143,3 +145,8 @@ private fun Series.defaultSeason(focusedSeasonId: UUID?): Season { private fun Season.nextUpEpisode(): Episode? { return episodes.firstOrNull { !it.watched } ?: episodes.firstOrNull() } + +private fun Season.focusTargetEpisodeId(focusedEpisodeId: UUID?): UUID? { + val focusedEpisode = episodes.firstOrNull { it.id == focusedEpisodeId } + return focusedEpisode?.id ?: nextUpEpisode()?.id +} diff --git a/app-tv/src/main/java/hu/bbara/purefin/ui/screen/series/components/TvSeriesComponents.kt b/app-tv/src/main/java/hu/bbara/purefin/ui/screen/series/components/TvSeriesComponents.kt index 13166b57..305932c5 100644 --- a/app-tv/src/main/java/hu/bbara/purefin/ui/screen/series/components/TvSeriesComponents.kt +++ b/app-tv/src/main/java/hu/bbara/purefin/ui/screen/series/components/TvSeriesComponents.kt @@ -82,7 +82,7 @@ internal fun TvSeasonTabs( seasons: List, selectedSeason: Season?, modifier: Modifier = Modifier, - firstItemFocusRequester: FocusRequester? = null, + selectedItemFocusRequester: FocusRequester? = null, firstItemTestTag: String? = null, onSelect: (Season) -> Unit ) { @@ -104,8 +104,8 @@ internal fun TvSeasonTabs( colors = TabDefaults.underlinedIndicatorTabColors(), modifier = Modifier .then( - if (index == 0 && firstItemFocusRequester != null) { - Modifier.focusRequester(firstItemFocusRequester) + if (index == selectedSeasonIndex && selectedItemFocusRequester != null) { + Modifier.focusRequester(selectedItemFocusRequester) } else { Modifier } @@ -145,6 +145,7 @@ internal fun TvEpisodeCarousel( ) { val listState = rememberLazyListState() val focusedEpisodeFocusRequester = remember { FocusRequester() } + var requestedFocusEpisodeId by remember { mutableStateOf(null) } LaunchedEffect(episodes, focusedEpisodeId) { val focusedEpisodeIndex = focusedEpisodeId?.let { id -> @@ -159,9 +160,10 @@ internal fun TvEpisodeCarousel( listState.scrollToItem(0) } - if (focusedEpisodeIndex >= 0) { + if (focusedEpisodeIndex >= 0 && requestedFocusEpisodeId != focusedEpisodeId) { withFrameNanos { } focusedEpisodeFocusRequester.requestFocus() + requestedFocusEpisodeId = focusedEpisodeId } }