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,
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,

View file

@ -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<String, WatchProgressEntry> = emptyMap(),
watchedKeys: Set<String> = 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<Int?>(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]

View file

@ -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),