feat: auto scroll to actively watching season

This commit is contained in:
tapframe 2026-04-07 12:16:59 +05:30
parent 61a558842f
commit 55d9bbe246
3 changed files with 97 additions and 19 deletions

View file

@ -501,6 +501,7 @@ fun MetaDetailsScreen(
isSaved = isSaved, isSaved = isSaved,
onPrimaryPlayClick = onPrimaryPlayClick, onPrimaryPlayClick = onPrimaryPlayClick,
onSaveClick = toggleSaved, onSaveClick = toggleSaved,
preferredEpisodeSeasonNumber = seriesAction?.seasonNumber,
hasProductionSection = hasProductionSection, hasProductionSection = hasProductionSection,
hasTrailersSection = hasTrailersSection, hasTrailersSection = hasTrailersSection,
hasEpisodes = hasEpisodes, hasEpisodes = hasEpisodes,
@ -795,6 +796,7 @@ private fun ConfiguredMetaSections(
isSaved: Boolean, isSaved: Boolean,
onPrimaryPlayClick: () -> Unit, onPrimaryPlayClick: () -> Unit,
onSaveClick: () -> Unit, onSaveClick: () -> Unit,
preferredEpisodeSeasonNumber: Int?,
hasProductionSection: Boolean, hasProductionSection: Boolean,
hasTrailersSection: Boolean, hasTrailersSection: Boolean,
hasEpisodes: Boolean, hasEpisodes: Boolean,
@ -887,6 +889,7 @@ private fun ConfiguredMetaSections(
DetailSeriesContent( DetailSeriesContent(
meta = meta, meta = meta,
showHeader = showHeader, showHeader = showHeader,
preferredSeasonNumber = preferredEpisodeSeasonNumber,
progressByVideoId = progressByVideoId, progressByVideoId = progressByVideoId,
watchedKeys = watchedKeys, watchedKeys = watchedKeys,
onEpisodeClick = onEpisodeClick, onEpisodeClick = onEpisodeClick,

View file

@ -27,11 +27,15 @@ import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width 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.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
@ -71,6 +75,7 @@ fun DetailSeriesContent(
meta: MetaDetails, meta: MetaDetails,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
showHeader: Boolean = true, showHeader: Boolean = true,
preferredSeasonNumber: Int? = null,
progressByVideoId: Map<String, WatchProgressEntry> = emptyMap(), progressByVideoId: Map<String, WatchProgressEntry> = emptyMap(),
watchedKeys: Set<String> = emptySet(), watchedKeys: Set<String> = emptySet(),
onEpisodeClick: ((MetaVideo) -> Unit)? = null, onEpisodeClick: ((MetaVideo) -> Unit)? = null,
@ -136,9 +141,13 @@ fun DetailSeriesContent(
} }
val seasons = groupedEpisodes.keys.sortedBy(::seasonSortKey) val seasons = groupedEpisodes.keys.sortedBy(::seasonSortKey)
val defaultSeason = seasons.first() val defaultSeason = preferredSeasonNumber
var selectedSeason by rememberSaveable(meta.id) { mutableStateOf(defaultSeason) } ?.takeIf { it in groupedEpisodes }
val currentSeason = selectedSeason.takeIf { it in groupedEpisodes } ?: defaultSeason ?: seasons.first()
var selectedSeasonOverride by rememberSaveable(meta.id) { mutableStateOf<Int?>(null) }
val currentSeason = selectedSeasonOverride
?.takeIf { it in groupedEpisodes }
?: defaultSeason
var seasonViewMode by remember { var seasonViewMode by remember {
mutableStateOf(SeasonViewModeStorage.load() ?: SeasonViewMode.Posters) mutableStateOf(SeasonViewModeStorage.load() ?: SeasonViewMode.Posters)
@ -199,13 +208,13 @@ fun DetailSeriesContent(
meta = meta, meta = meta,
currentSeason = currentSeason, currentSeason = currentSeason,
sizing = sizing, sizing = sizing,
onSelect = { selectedSeason = it }, onSelect = { selectedSeasonOverride = it },
) )
SeasonViewMode.Text -> SeasonTextChipScrollRow( SeasonViewMode.Text -> SeasonTextChipScrollRow(
seasons = seasons, seasons = seasons,
currentSeason = currentSeason, currentSeason = currentSeason,
sizing = sizing, sizing = sizing,
onSelect = { selectedSeason = it }, onSelect = { selectedSeasonOverride = it },
) )
} }
} }
@ -214,7 +223,7 @@ fun DetailSeriesContent(
seasons = seasons, seasons = seasons,
currentSeason = currentSeason, currentSeason = currentSeason,
sizing = sizing, sizing = sizing,
onSelect = { selectedSeason = it }, onSelect = { selectedSeasonOverride = it },
) )
} }
} }
@ -325,13 +334,27 @@ private fun SeasonTextChipScrollRow(
sizing: SeriesContentSizing, sizing: SeriesContentSizing,
onSelect: (Int) -> Unit, onSelect: (Int) -> Unit,
) { ) {
Row( val seasonListState = rememberLazyListState()
modifier = Modifier var hasPositionedSeasonRow by remember(seasons) { mutableStateOf(false) }
.fillMaxWidth()
.horizontalScroll(rememberScrollState()), 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), horizontalArrangement = Arrangement.spacedBy(sizing.seasonChipGap),
) { ) {
seasons.forEach { season -> items(seasons, key = { season -> season }) { season ->
val isSelected = season == currentSeason val isSelected = season == currentSeason
Box( Box(
modifier = Modifier modifier = Modifier
@ -376,13 +399,27 @@ private fun SeasonPosterScrollRow(
sizing: SeriesContentSizing, sizing: SeriesContentSizing,
onSelect: (Int) -> Unit, onSelect: (Int) -> Unit,
) { ) {
Row( val seasonListState = rememberLazyListState()
modifier = Modifier var hasPositionedSeasonRow by remember(seasons) { mutableStateOf(false) }
.fillMaxWidth()
.horizontalScroll(rememberScrollState()), 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), horizontalArrangement = Arrangement.spacedBy(sizing.seasonChipGap),
) { ) {
seasons.forEach { season -> items(seasons, key = { season -> season }) { season ->
SeasonPosterButton( SeasonPosterButton(
label = season.label(), label = season.label(),
imageUrl = groupedEpisodes[season] imageUrl = groupedEpisodes[season]

View file

@ -25,7 +25,9 @@ import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
@ -37,6 +39,7 @@ import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
@ -183,6 +186,40 @@ private fun EpisodesListSubView(
(groupedEpisodes[selectedSeason] ?: emptyList()) (groupedEpisodes[selectedSeason] ?: emptyList())
.sortedBy { it.episode ?: 0 } .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 { Column {
// Header // Header
@ -204,15 +241,15 @@ private fun EpisodesListSubView(
// Season tabs // Season tabs
if (availableSeasons.size > 1) { if (availableSeasons.size > 1) {
Row( LazyRow(
state = seasonListState,
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.horizontalScroll(rememberScrollState())
.padding(horizontal = 20.dp) .padding(horizontal = 20.dp)
.padding(bottom = 12.dp), .padding(bottom = 12.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp), horizontalArrangement = Arrangement.spacedBy(8.dp),
) { ) {
availableSeasons.forEach { season -> items(availableSeasons, key = { season -> season }) { season ->
val label = if (season == 0) "Specials" else "Season $season" val label = if (season == 0) "Specials" else "Season $season"
AddonFilterChip( AddonFilterChip(
label = label, label = label,
@ -242,6 +279,7 @@ private fun EpisodesListSubView(
} }
} else { } else {
LazyColumn( LazyColumn(
state = episodeListState,
modifier = Modifier.padding(horizontal = 12.dp), modifier = Modifier.padding(horizontal = 12.dp),
verticalArrangement = Arrangement.spacedBy(4.dp), verticalArrangement = Arrangement.spacedBy(4.dp),
contentPadding = androidx.compose.foundation.layout.PaddingValues(bottom = 16.dp), contentPadding = androidx.compose.foundation.layout.PaddingValues(bottom = 16.dp),