diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/MetaDetailsScreen.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/MetaDetailsScreen.kt index 8636ec34..097f212f 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/MetaDetailsScreen.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/MetaDetailsScreen.kt @@ -501,6 +501,7 @@ fun MetaDetailsScreen( isSaved = isSaved, onPrimaryPlayClick = onPrimaryPlayClick, onSaveClick = toggleSaved, + preferredEpisodeSeasonNumber = seriesAction?.seasonNumber, hasProductionSection = hasProductionSection, hasTrailersSection = hasTrailersSection, hasEpisodes = hasEpisodes, @@ -795,6 +796,7 @@ private fun ConfiguredMetaSections( isSaved: Boolean, onPrimaryPlayClick: () -> Unit, onSaveClick: () -> Unit, + preferredEpisodeSeasonNumber: Int?, hasProductionSection: Boolean, hasTrailersSection: Boolean, hasEpisodes: Boolean, @@ -887,6 +889,7 @@ private fun ConfiguredMetaSections( DetailSeriesContent( meta = meta, showHeader = showHeader, + preferredSeasonNumber = preferredEpisodeSeasonNumber, progressByVideoId = progressByVideoId, watchedKeys = watchedKeys, onEpisodeClick = onEpisodeClick, diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/components/DetailSeriesContent.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/components/DetailSeriesContent.kt index f39c2034..9d3ca3bf 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/components/DetailSeriesContent.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/components/DetailSeriesContent.kt @@ -27,11 +27,15 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -71,6 +75,7 @@ fun DetailSeriesContent( meta: MetaDetails, modifier: Modifier = Modifier, showHeader: Boolean = true, + preferredSeasonNumber: Int? = null, progressByVideoId: Map = emptyMap(), watchedKeys: Set = emptySet(), onEpisodeClick: ((MetaVideo) -> Unit)? = null, @@ -136,9 +141,13 @@ fun DetailSeriesContent( } val seasons = groupedEpisodes.keys.sortedBy(::seasonSortKey) - val defaultSeason = seasons.first() - var selectedSeason by rememberSaveable(meta.id) { mutableStateOf(defaultSeason) } - val currentSeason = selectedSeason.takeIf { it in groupedEpisodes } ?: defaultSeason + val defaultSeason = preferredSeasonNumber + ?.takeIf { it in groupedEpisodes } + ?: seasons.first() + var selectedSeasonOverride by rememberSaveable(meta.id) { mutableStateOf(null) } + val currentSeason = selectedSeasonOverride + ?.takeIf { it in groupedEpisodes } + ?: defaultSeason var seasonViewMode by remember { mutableStateOf(SeasonViewModeStorage.load() ?: SeasonViewMode.Posters) @@ -199,13 +208,13 @@ fun DetailSeriesContent( meta = meta, currentSeason = currentSeason, sizing = sizing, - onSelect = { selectedSeason = it }, + onSelect = { selectedSeasonOverride = it }, ) SeasonViewMode.Text -> SeasonTextChipScrollRow( seasons = seasons, currentSeason = currentSeason, sizing = sizing, - onSelect = { selectedSeason = it }, + onSelect = { selectedSeasonOverride = it }, ) } } @@ -214,7 +223,7 @@ fun DetailSeriesContent( seasons = seasons, currentSeason = currentSeason, sizing = sizing, - onSelect = { selectedSeason = it }, + onSelect = { selectedSeasonOverride = it }, ) } } @@ -325,13 +334,27 @@ private fun SeasonTextChipScrollRow( sizing: SeriesContentSizing, onSelect: (Int) -> Unit, ) { - Row( - modifier = Modifier - .fillMaxWidth() - .horizontalScroll(rememberScrollState()), + val seasonListState = rememberLazyListState() + var hasPositionedSeasonRow by remember(seasons) { mutableStateOf(false) } + + LaunchedEffect(seasons, currentSeason) { + val currentIndex = seasons.indexOf(currentSeason) + if (currentIndex >= 0) { + if (hasPositionedSeasonRow) { + seasonListState.animateScrollToItem(currentIndex) + } else { + seasonListState.scrollToItem(currentIndex) + hasPositionedSeasonRow = true + } + } + } + + LazyRow( + state = seasonListState, + modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(sizing.seasonChipGap), ) { - seasons.forEach { season -> + items(seasons, key = { season -> season }) { season -> val isSelected = season == currentSeason Box( modifier = Modifier @@ -376,13 +399,27 @@ private fun SeasonPosterScrollRow( sizing: SeriesContentSizing, onSelect: (Int) -> Unit, ) { - Row( - modifier = Modifier - .fillMaxWidth() - .horizontalScroll(rememberScrollState()), + val seasonListState = rememberLazyListState() + var hasPositionedSeasonRow by remember(seasons) { mutableStateOf(false) } + + LaunchedEffect(seasons, currentSeason) { + val currentIndex = seasons.indexOf(currentSeason) + if (currentIndex >= 0) { + if (hasPositionedSeasonRow) { + seasonListState.animateScrollToItem(currentIndex) + } else { + seasonListState.scrollToItem(currentIndex) + hasPositionedSeasonRow = true + } + } + } + + LazyRow( + state = seasonListState, + modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(sizing.seasonChipGap), ) { - seasons.forEach { season -> + items(seasons, key = { season -> season }) { season -> SeasonPosterButton( label = season.label(), imageUrl = groupedEpisodes[season] diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/PlayerEpisodesPanel.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/PlayerEpisodesPanel.kt index ecec11c5..a9a7fd52 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/PlayerEpisodesPanel.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/PlayerEpisodesPanel.kt @@ -25,7 +25,9 @@ import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons @@ -37,6 +39,7 @@ import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf @@ -183,6 +186,40 @@ private fun EpisodesListSubView( (groupedEpisodes[selectedSeason] ?: emptyList()) .sortedBy { it.episode ?: 0 } } + val seasonListState = rememberLazyListState() + val episodeListState = rememberLazyListState() + var hasPositionedSeasonRow by remember(availableSeasons) { mutableStateOf(false) } + var hasPositionedEpisodeList by remember(selectedSeason) { mutableStateOf(false) } + + LaunchedEffect(selectedSeason, availableSeasons) { + val selectedSeasonIndex = availableSeasons.indexOf(selectedSeason) + if (selectedSeasonIndex >= 0) { + if (hasPositionedSeasonRow) { + seasonListState.animateScrollToItem(selectedSeasonIndex) + } else { + seasonListState.scrollToItem(selectedSeasonIndex) + hasPositionedSeasonRow = true + } + } + } + + LaunchedEffect(selectedSeason, seasonEpisodes, currentSeason, currentEpisode) { + if (seasonEpisodes.isEmpty()) return@LaunchedEffect + val activeEpisodeIndex = if (selectedSeason == currentSeason && currentEpisode != null) { + seasonEpisodes.indexOfFirst { episode -> + episode.season == currentSeason && episode.episode == currentEpisode + } + } else { + -1 + } + val targetIndex = activeEpisodeIndex.takeIf { it >= 0 } ?: 0 + if (hasPositionedEpisodeList) { + episodeListState.animateScrollToItem(targetIndex) + } else { + episodeListState.scrollToItem(targetIndex) + hasPositionedEpisodeList = true + } + } Column { // Header @@ -204,15 +241,15 @@ private fun EpisodesListSubView( // Season tabs if (availableSeasons.size > 1) { - Row( + LazyRow( + state = seasonListState, modifier = Modifier .fillMaxWidth() - .horizontalScroll(rememberScrollState()) .padding(horizontal = 20.dp) .padding(bottom = 12.dp), horizontalArrangement = Arrangement.spacedBy(8.dp), ) { - availableSeasons.forEach { season -> + items(availableSeasons, key = { season -> season }) { season -> val label = if (season == 0) "Specials" else "Season $season" AddonFilterChip( label = label, @@ -242,6 +279,7 @@ private fun EpisodesListSubView( } } else { LazyColumn( + state = episodeListState, modifier = Modifier.padding(horizontal = 12.dp), verticalArrangement = Arrangement.spacedBy(4.dp), contentPadding = androidx.compose.foundation.layout.PaddingValues(bottom = 16.dp),