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)
}
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
}

View File

@@ -82,7 +82,7 @@ internal fun TvSeasonTabs(
seasons: List<Season>,
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<UUID?>(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
}
}