feat: enhance TvSeriesScreen to support focused season and episode loading

This commit is contained in:
2026-05-08 19:44:49 +02:00
parent 4788eec102
commit 9313cded53
2 changed files with 28 additions and 26 deletions

View File

@@ -41,6 +41,7 @@ fun EntryProviderScope<Route>.tvEpisodeSection() {
entry<Route.EpisodeRoute> { route -> entry<Route.EpisodeRoute> { route ->
TvSeriesScreen( TvSeriesScreen(
series = SeriesDto(id = route.item.seriesId), series = SeriesDto(id = route.item.seriesId),
focusedSeasonId = route.item.seasonId,
focusedEpisodeId = route.item.id focusedEpisodeId = route.item.id
) )
} }

View File

@@ -37,6 +37,7 @@ import java.util.UUID
@Composable @Composable
fun TvSeriesScreen( fun TvSeriesScreen(
series: SeriesDto, series: SeriesDto,
focusedSeasonId: UUID? = null,
focusedEpisodeId: UUID? = null, focusedEpisodeId: UUID? = null,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
viewModel: SeriesViewModel = hiltViewModel() viewModel: SeriesViewModel = hiltViewModel()
@@ -52,6 +53,8 @@ fun TvSeriesScreen(
TvSeriesScreenContent( TvSeriesScreenContent(
series = seriesData, series = seriesData,
onPlayEpisode = viewModel::onPlayEpisode, onPlayEpisode = viewModel::onPlayEpisode,
onLoadSeasonEpisodes = viewModel::loadSeasonEpisodes,
focusedSeasonId = focusedSeasonId,
focusedEpisodeId = focusedEpisodeId, focusedEpisodeId = focusedEpisodeId,
modifier = modifier modifier = modifier
) )
@@ -64,25 +67,28 @@ fun TvSeriesScreen(
internal fun TvSeriesScreenContent( internal fun TvSeriesScreenContent(
series: Series, series: Series,
onPlayEpisode: (UUID) -> Unit, onPlayEpisode: (UUID) -> Unit,
onLoadSeasonEpisodes: (UUID, UUID) -> Unit = { _, _ -> },
focusedSeasonId: UUID? = null,
focusedEpisodeId: UUID? = null, focusedEpisodeId: UUID? = null,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
val nextUpEpisode = remember(series.id) { series.nextUpEpisode() } val defaultSeason = series.defaultSeason(focusedSeasonId)
val focusedEpisode = remember(series.id, focusedEpisodeId) { var selectedSeasonId by remember(series.id, focusedSeasonId) {
focusedEpisodeId?.let { id -> mutableStateOf(defaultSeason.id)
series.seasons
.flatMap { it.episodes }
.firstOrNull { it.id == id }
}
} }
val initialEpisode = focusedEpisode ?: nextUpEpisode val selectedSeason = series.seasons.firstOrNull { it.id == selectedSeasonId } ?: defaultSeason
var selectedSeason by remember(series.id, initialEpisode?.seasonId) { val initialFocusedEpisodeId = remember(series.id, focusedSeasonId, focusedEpisodeId) {
mutableStateOf(series.defaultSeason(initialEpisode)) val focusedEpisode = defaultSeason.episodes.firstOrNull { it.id == focusedEpisodeId }
focusedEpisode?.id ?: defaultSeason.nextUpEpisode()?.id
} }
val firstContentFocusRequester = remember { FocusRequester() } val firstContentFocusRequester = remember { FocusRequester() }
LaunchedEffect(series.id, initialEpisode?.id) { LaunchedEffect(series.id, selectedSeason.id) {
if (initialEpisode != null) return@LaunchedEffect onLoadSeasonEpisodes(series.id, selectedSeason.id)
}
LaunchedEffect(series.id, initialFocusedEpisodeId) {
if (initialFocusedEpisodeId != null) return@LaunchedEffect
withFrameNanos { } withFrameNanos { }
firstContentFocusRequester.requestFocus() firstContentFocusRequester.requestFocus()
} }
@@ -111,14 +117,14 @@ internal fun TvSeriesScreenContent(
selectedSeason = selectedSeason, selectedSeason = selectedSeason,
firstItemFocusRequester = firstContentFocusRequester, firstItemFocusRequester = firstContentFocusRequester,
firstItemTestTag = SeriesFirstSeasonTabTag, firstItemTestTag = SeriesFirstSeasonTabTag,
onSelect = { selectedSeason = it }, onSelect = { selectedSeasonId = it.id },
modifier = Modifier.fillMaxWidth() modifier = Modifier.fillMaxWidth()
) )
Spacer(modifier = Modifier.height(16.dp)) Spacer(modifier = Modifier.height(16.dp))
TvEpisodeCarousel( TvEpisodeCarousel(
episodes = selectedSeason.episodes, episodes = selectedSeason.episodes,
onPlayEpisode = { onPlayEpisode(it.id) }, onPlayEpisode = { onPlayEpisode(it.id) },
focusedEpisodeId = initialEpisode?.id, focusedEpisodeId = initialFocusedEpisodeId,
modifier = Modifier.fillMaxWidth() modifier = Modifier.fillMaxWidth()
) )
} }
@@ -126,19 +132,14 @@ internal fun TvSeriesScreenContent(
} }
} }
private fun Series.defaultSeason(nextUpEpisode: Episode?): Season { private fun Series.defaultSeason(focusedSeasonId: UUID?): Season {
if (nextUpEpisode != null) { if (focusedSeasonId != null) {
seasons.firstOrNull { it.id == nextUpEpisode.seasonId }?.let { return it } seasons.firstOrNull { it.id == focusedSeasonId }?.let { return it }
} }
for (season in seasons) { return seasons.firstOrNull { it.unwatchedEpisodeCount > 0 } ?: seasons.first()
if (season.episodes.any { !it.watched }) {
return season
}
}
return seasons.first()
} }
private fun Series.nextUpEpisode() = seasons.firstNotNullOfOrNull { season -> private fun Season.nextUpEpisode(): Episode? {
season.episodes.firstOrNull { !it.watched } return episodes.firstOrNull { !it.watched } ?: episodes.firstOrNull()
} ?: seasons.firstOrNull()?.episodes?.firstOrNull() }