feat(tvseries): improve focus management for season and episode selection

This commit is contained in:
2026-05-09 09:03:07 +02:00
parent 4e9d8c0b3b
commit 406d0f4ed8
2 changed files with 22 additions and 13 deletions

View File

@@ -77,20 +77,22 @@ internal fun TvSeriesScreenContent(
mutableStateOf(defaultSeason.id) mutableStateOf(defaultSeason.id)
} }
val selectedSeason = series.seasons.firstOrNull { it.id == selectedSeasonId } ?: defaultSeason val selectedSeason = series.seasons.firstOrNull { it.id == selectedSeasonId } ?: defaultSeason
val initialFocusedEpisodeId = remember(series.id, focusedSeasonId, focusedEpisodeId) { val initialFocusSeasonId = remember(series.id, focusedSeasonId) { defaultSeason.id }
val focusedEpisode = defaultSeason.episodes.firstOrNull { it.id == focusedEpisodeId } val initialFocusSeason = series.seasons.firstOrNull { it.id == initialFocusSeasonId } ?: defaultSeason
focusedEpisode?.id ?: defaultSeason.nextUpEpisode()?.id val initialFocusedEpisodeId = initialFocusSeason.focusTargetEpisodeId(focusedEpisodeId)
} val seasonTabFocusRequester = remember { FocusRequester() }
val firstContentFocusRequester = remember { FocusRequester() } val waitingForInitialEpisodes = initialFocusedEpisodeId == null &&
initialFocusSeason.episodes.isEmpty() &&
initialFocusSeason.episodeCount > 0
LaunchedEffect(series.id, selectedSeason.id) { LaunchedEffect(series.id, selectedSeason.id) {
onLoadSeasonEpisodes(series.id, selectedSeason.id) onLoadSeasonEpisodes(series.id, selectedSeason.id)
} }
LaunchedEffect(series.id, initialFocusedEpisodeId) { LaunchedEffect(series.id, initialFocusedEpisodeId, waitingForInitialEpisodes) {
if (initialFocusedEpisodeId != null) return@LaunchedEffect if (initialFocusedEpisodeId != null || waitingForInitialEpisodes) return@LaunchedEffect
withFrameNanos { } withFrameNanos { }
firstContentFocusRequester.requestFocus() seasonTabFocusRequester.requestFocus()
} }
TvMediaDetailScaffold( TvMediaDetailScaffold(
@@ -115,7 +117,7 @@ internal fun TvSeriesScreenContent(
TvSeasonTabs( TvSeasonTabs(
seasons = series.seasons, seasons = series.seasons,
selectedSeason = selectedSeason, selectedSeason = selectedSeason,
firstItemFocusRequester = firstContentFocusRequester, selectedItemFocusRequester = seasonTabFocusRequester,
firstItemTestTag = SeriesFirstSeasonTabTag, firstItemTestTag = SeriesFirstSeasonTabTag,
onSelect = { selectedSeasonId = it.id }, onSelect = { selectedSeasonId = it.id },
modifier = Modifier.fillMaxWidth() modifier = Modifier.fillMaxWidth()
@@ -143,3 +145,8 @@ private fun Series.defaultSeason(focusedSeasonId: UUID?): Season {
private fun Season.nextUpEpisode(): Episode? { private fun Season.nextUpEpisode(): Episode? {
return episodes.firstOrNull { !it.watched } ?: episodes.firstOrNull() 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
}

View File

@@ -82,7 +82,7 @@ internal fun TvSeasonTabs(
seasons: List<Season>, seasons: List<Season>,
selectedSeason: Season?, selectedSeason: Season?,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
firstItemFocusRequester: FocusRequester? = null, selectedItemFocusRequester: FocusRequester? = null,
firstItemTestTag: String? = null, firstItemTestTag: String? = null,
onSelect: (Season) -> Unit onSelect: (Season) -> Unit
) { ) {
@@ -104,8 +104,8 @@ internal fun TvSeasonTabs(
colors = TabDefaults.underlinedIndicatorTabColors(), colors = TabDefaults.underlinedIndicatorTabColors(),
modifier = Modifier modifier = Modifier
.then( .then(
if (index == 0 && firstItemFocusRequester != null) { if (index == selectedSeasonIndex && selectedItemFocusRequester != null) {
Modifier.focusRequester(firstItemFocusRequester) Modifier.focusRequester(selectedItemFocusRequester)
} else { } else {
Modifier Modifier
} }
@@ -145,6 +145,7 @@ internal fun TvEpisodeCarousel(
) { ) {
val listState = rememberLazyListState() val listState = rememberLazyListState()
val focusedEpisodeFocusRequester = remember { FocusRequester() } val focusedEpisodeFocusRequester = remember { FocusRequester() }
var requestedFocusEpisodeId by remember { mutableStateOf<UUID?>(null) }
LaunchedEffect(episodes, focusedEpisodeId) { LaunchedEffect(episodes, focusedEpisodeId) {
val focusedEpisodeIndex = focusedEpisodeId?.let { id -> val focusedEpisodeIndex = focusedEpisodeId?.let { id ->
@@ -159,9 +160,10 @@ internal fun TvEpisodeCarousel(
listState.scrollToItem(0) listState.scrollToItem(0)
} }
if (focusedEpisodeIndex >= 0) { if (focusedEpisodeIndex >= 0 && requestedFocusEpisodeId != focusedEpisodeId) {
withFrameNanos { } withFrameNanos { }
focusedEpisodeFocusRequester.requestFocus() focusedEpisodeFocusRequester.requestFocus()
requestedFocusEpisodeId = focusedEpisodeId
} }
} }