feature: add content progress and autoscroll to next up media to SeriesComponents

This commit is contained in:
2026-01-26 16:58:38 +01:00
parent f835d9ea26
commit 3dc9ec7524
6 changed files with 59 additions and 18 deletions

View File

@@ -18,21 +18,27 @@ object ContentMockData {
title = "E1: The Beginning", title = "E1: The Beginning",
description = "The crew assembles for the first time as the anomaly begins to expand rapidly near Saturn's rings.", description = "The crew assembles for the first time as the anomaly begins to expand rapidly near Saturn's rings.",
duration = "58m", duration = "58m",
imageUrl = "https://lh3.googleusercontent.com/aida-public/AB6AXuC6OPszCXCIP_FMO3BJJUrjpCtDNw9aeHYOGyOAXdqF078hDFNrH7KXbaQ7qtipz6aIPLivd8VBBffNMbeAiYIjjWjn5GMb6Xn9iiJz0D2rzhCKi0TBeFrN6tC1IXJkzQyQKJNhTnyokWy9dd-YtN65V7er7RT6hP5jdVBXhtK1xZMjlgrm1bk_FTTmKd8Afu3zPtJCaaC98Z608vav5zhYlkrdA1wKNSTWTpzwMSyDIY3pNQNPFauWf0n-iEu7QsYTAwhCG_zfxz0" imageUrl = "https://lh3.googleusercontent.com/aida-public/AB6AXuC6OPszCXCIP_FMO3BJJUrjpCtDNw9aeHYOGyOAXdqF078hDFNrH7KXbaQ7qtipz6aIPLivd8VBBffNMbeAiYIjjWjn5GMb6Xn9iiJz0D2rzhCKi0TBeFrN6tC1IXJkzQyQKJNhTnyokWy9dd-YtN65V7er7RT6hP5jdVBXhtK1xZMjlgrm1bk_FTTmKd8Afu3zPtJCaaC98Z608vav5zhYlkrdA1wKNSTWTpzwMSyDIY3pNQNPFauWf0n-iEu7QsYTAwhCG_zfxz0",
progress = 40.0,
watched = false
) )
val episode2 = SeriesEpisodeUiModel( val episode2 = SeriesEpisodeUiModel(
id = "2", id = "2",
title = "E2: Event Horizon", title = "E2: Event Horizon",
description = "Dr. Cole discovers a frequency embedded in the rift's radiation that suggests intelligent design.", description = "Dr. Cole discovers a frequency embedded in the rift's radiation that suggests intelligent design.",
duration = "54m", duration = "54m",
imageUrl = "https://lh3.googleusercontent.com/aida-public/AB6AXuBExsf-wEzAVjMxasU2ImGhlreqQo9biBSN1yHyAbW8MyuhuppRw9ho7OD3vsbySSJ3kNluEgH1Qun45PmLnZWixZsFU4Qc7UGGJNKMS5Nkm4GZAsKdFvb3z_i1tkCvaXXvGpqmwI0qjFuo1QyjjhYPA5Yp3I8ZhrnDYdQv_GxbhR6Vl3mY1rbxd2BIUEE5oMTwTF-QmJztUEaViZkSGSG2VgVXZ5VAREn4xWE902OH2sysllvXQJQIaj439JIC2_Vg61m0-F-F1Vc" imageUrl = "https://lh3.googleusercontent.com/aida-public/AB6AXuBExsf-wEzAVjMxasU2ImGhlreqQo9biBSN1yHyAbW8MyuhuppRw9ho7OD3vsbySSJ3kNluEgH1Qun45PmLnZWixZsFU4Qc7UGGJNKMS5Nkm4GZAsKdFvb3z_i1tkCvaXXvGpqmwI0qjFuo1QyjjhYPA5Yp3I8ZhrnDYdQv_GxbhR6Vl3mY1rbxd2BIUEE5oMTwTF-QmJztUEaViZkSGSG2VgVXZ5VAREn4xWE902OH2sysllvXQJQIaj439JIC2_Vg61m0-F-F1Vc",
progress = 100.0,
watched = true
) )
val episode3 = SeriesEpisodeUiModel( val episode3 = SeriesEpisodeUiModel(
id = "3", id = "3",
title = "E3: Singularity", title = "E3: Singularity",
description = "Tension rises as the ship approaches the event horizon, and the AI begins to behave erratically.", description = "Tension rises as the ship approaches the event horizon, and the AI begins to behave erratically.",
duration = "1h 02m", duration = "1h 02m",
imageUrl = "https://lh3.googleusercontent.com/aida-public/AB6AXuA5CFDWsWYO4YxdRoLd2QfH5Su2KLhtj5xSDb8qmzWHvPE888ac_HAAj1wu1uqdFNSncdmmJ-bWsc--h6NYKxVXkhd4vHaFWi0XTJXgsR0F3cBu_l2SynSX4TMNSy5C3XWDurgeSH789byOe1HvoxHCHTJYaSf3OyEbil-NOp9g_9mZ24CIZOI79nx57CRzmooxoswycqssPpfTNkrnoYrrAczt5qbncwLM9NVU442YxyBFisr2Ds9H-CNBOakiCtaKnoJ6npznM7U" imageUrl = "https://lh3.googleusercontent.com/aida-public/AB6AXuA5CFDWsWYO4YxdRoLd2QfH5Su2KLhtj5xSDb8qmzWHvPE888ac_HAAj1wu1uqdFNSncdmmJ-bWsc--h6NYKxVXkhd4vHaFWi0XTJXgsR0F3cBu_l2SynSX4TMNSy5C3XWDurgeSH789byOe1HvoxHCHTJYaSf3OyEbil-NOp9g_9mZ24CIZOI79nx57CRzmooxoswycqssPpfTNkrnoYrrAczt5qbncwLM9NVU442YxyBFisr2Ds9H-CNBOakiCtaKnoJ6npznM7U",
progress = 40.0,
watched = false
) )
return SeriesUiModel( return SeriesUiModel(
title = "Interstellar Horizon: The Series", title = "Interstellar Horizon: The Series",
@@ -45,18 +51,18 @@ object ContentMockData {
seasonTabs = listOf( seasonTabs = listOf(
SeriesSeasonUiModel( SeriesSeasonUiModel(
name = "Season 1", name = "Season 1",
isSelected = true, episodes = listOf(episode1, episode2, episode3),
episodes = listOf(episode1, episode2, episode3) unplayedCount = 2
), ),
SeriesSeasonUiModel( SeriesSeasonUiModel(
name = "Season 2", name = "Season 2",
isSelected = false, episodes = listOf(episode1, episode2, episode3),
episodes = listOf(episode1, episode2, episode3) unplayedCount = 0
), ),
SeriesSeasonUiModel( SeriesSeasonUiModel(
name = "Season 3", name = "Season 3",
isSelected = false, episodes = listOf(episode1, episode2, episode3),
episodes = listOf(episode1, episode2, episode3) unplayedCount = 1
) )
), ),
cast = listOf( cast = listOf(

View File

@@ -21,6 +21,7 @@ import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyRow 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
@@ -34,6 +35,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.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
@@ -169,9 +171,20 @@ private fun SeasonTab(
@Composable @Composable
internal fun EpisodeCarousel(episodes: List<SeriesEpisodeUiModel>, modifier: Modifier = Modifier) { internal fun EpisodeCarousel(episodes: List<SeriesEpisodeUiModel>, modifier: Modifier = Modifier) {
val listState = rememberLazyListState()
LaunchedEffect(episodes) {
val firstUnwatchedIndex = episodes.indexOfFirst { !it.watched }.let { if (it == -1) 0 else it }
if (firstUnwatchedIndex != 0) {
listState.animateScrollToItem(firstUnwatchedIndex)
} else {
listState.scrollToItem(0)
}
}
LazyRow( LazyRow(
state = listState,
modifier = modifier, modifier = modifier,
// contentPadding = PaddingValues(horizontal = 20.dp),
horizontalArrangement = Arrangement.spacedBy(16.dp) horizontalArrangement = Arrangement.spacedBy(16.dp)
) { ) {
items(episodes) { episode -> items(episodes) { episode ->

View File

@@ -5,13 +5,15 @@ data class SeriesEpisodeUiModel(
val title: String, val title: String,
val description: String, val description: String,
val duration: String, val duration: String,
val imageUrl: String val imageUrl: String,
val watched: Boolean,
val progress: Double?
) )
data class SeriesSeasonUiModel( data class SeriesSeasonUiModel(
val name: String, val name: String,
val isSelected: Boolean, val episodes: List<SeriesEpisodeUiModel>,
val episodes: List<SeriesEpisodeUiModel> val unplayedCount: Int?
) )
data class SeriesCastMemberUiModel( data class SeriesCastMemberUiModel(
@@ -30,4 +32,15 @@ data class SeriesUiModel(
val heroImageUrl: String, val heroImageUrl: String,
val seasonTabs: List<SeriesSeasonUiModel>, val seasonTabs: List<SeriesSeasonUiModel>,
val cast: List<SeriesCastMemberUiModel> val cast: List<SeriesCastMemberUiModel>
) ) {
fun getNextEpisode(): SeriesEpisodeUiModel {
for (season in seasonTabs) {
for (episode in season.episodes) {
if (!episode.watched) {
return episode
}
}
}
return seasonTabs.first().episodes.first()
}
}

View File

@@ -61,7 +61,14 @@ private fun SeriesScreenInternal(
val textMutedStrong = scheme.onSurfaceVariant.copy(alpha = 0.7f) val textMutedStrong = scheme.onSurfaceVariant.copy(alpha = 0.7f)
fun getDefaultSeason() : SeriesSeasonUiModel { fun getDefaultSeason() : SeriesSeasonUiModel {
// TODO get next next episodes season selected or add logic to it. for (season in series.seasonTabs) {
val firstUnwatchedEpisode = season.episodes.firstOrNull {
it.watched.not()
}
if (firstUnwatchedEpisode != null) {
return season
}
}
return series.seasonTabs.first() return series.seasonTabs.first()
} }
val selectedSeason = remember { mutableStateOf<SeriesSeasonUiModel>(getDefaultSeason()) } val selectedSeason = remember { mutableStateOf<SeriesSeasonUiModel>(getDefaultSeason()) }

View File

@@ -79,14 +79,15 @@ class SeriesViewModel @Inject constructor(
title = episode.name ?: "Unknown", title = episode.name ?: "Unknown",
description = episode.overview ?: "", description = episode.overview ?: "",
duration = "58m", duration = "58m",
imageUrl = JellyfinImageHelper.toImageUrl(url = serverUrl, itemId = episode.id, type = ImageType.PRIMARY) imageUrl = JellyfinImageHelper.toImageUrl(url = serverUrl, itemId = episode.id, type = ImageType.PRIMARY),
progress = episode.userData!!.playedPercentage,
watched = episode.userData!!.played
) )
} }
SeriesSeasonUiModel( SeriesSeasonUiModel(
name = season.name ?: "Unknown", name = season.name ?: "Unknown",
episodes = episodeItemUiModels, episodes = episodeItemUiModels,
// TODO add actual logic or remove unplayedCount = season.userData!!.unplayedItemCount
isSelected = false,
) )
} }
return SeriesUiModel( return SeriesUiModel(

View File

@@ -167,6 +167,7 @@ class JellyfinApiClient @Inject constructor(
val result = api.tvShowsApi.getSeasons( val result = api.tvShowsApi.getSeasons(
userId = getUserId(), userId = getUserId(),
seriesId = seriesId, seriesId = seriesId,
enableUserData = true
) )
Log.d("getSeasons response: {}", result.content.toString()) Log.d("getSeasons response: {}", result.content.toString())
return result.content.items return result.content.items