mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-05-04 17:29:07 +00:00
feat: auto scroll to actively watching season
This commit is contained in:
parent
61a558842f
commit
55d9bbe246
3 changed files with 97 additions and 19 deletions
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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]
|
||||||
|
|
|
||||||
|
|
@ -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),
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue