tv series season details screen

This commit is contained in:
Owen LeJeune
2023-07-25 19:27:39 -04:00
parent dcb24561ec
commit 3df94cbd70
15 changed files with 536 additions and 246 deletions

View File

@@ -10,7 +10,7 @@ import com.owenlejeune.tvtime.api.tmdb.api.v3.model.DetailCrew
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.DetailPerson import com.owenlejeune.tvtime.api.tmdb.api.v3.model.DetailPerson
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.ExternalIds import com.owenlejeune.tvtime.api.tmdb.api.v3.model.ExternalIds
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.HomePagePeopleResponse import com.owenlejeune.tvtime.api.tmdb.api.v3.model.HomePagePeopleResponse
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.PersonImage import com.owenlejeune.tvtime.api.tmdb.api.v3.model.Image
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.SearchResult import com.owenlejeune.tvtime.api.tmdb.api.v3.model.SearchResult
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.SearchResultPerson import com.owenlejeune.tvtime.api.tmdb.api.v3.model.SearchResultPerson
import com.owenlejeune.tvtime.utils.types.TimeWindow import com.owenlejeune.tvtime.utils.types.TimeWindow
@@ -27,7 +27,7 @@ class PeopleService: KoinComponent {
val peopleMap = Collections.synchronizedMap(mutableStateMapOf<Int, DetailPerson>()) val peopleMap = Collections.synchronizedMap(mutableStateMapOf<Int, DetailPerson>())
val castMap = Collections.synchronizedMap(mutableStateMapOf<Int, List<DetailCast>>()) val castMap = Collections.synchronizedMap(mutableStateMapOf<Int, List<DetailCast>>())
val crewMap = Collections.synchronizedMap(mutableStateMapOf<Int, List<DetailCrew>>()) val crewMap = Collections.synchronizedMap(mutableStateMapOf<Int, List<DetailCrew>>())
val imagesMap = Collections.synchronizedMap(mutableStateMapOf<Int, List<PersonImage>>()) val imagesMap = Collections.synchronizedMap(mutableStateMapOf<Int, List<Image>>())
val externalIdsMap = Collections.synchronizedMap(mutableStateMapOf<Int, ExternalIds>()) val externalIdsMap = Collections.synchronizedMap(mutableStateMapOf<Int, ExternalIds>())
val detailsLoadingState = mutableStateOf(LoadingState.INACTIVE) val detailsLoadingState = mutableStateOf(LoadingState.INACTIVE)

View File

@@ -73,4 +73,19 @@ interface TvApi {
@GET("tv/{id}/season/{season}") @GET("tv/{id}/season/{season}")
suspend fun getSeason(@Path("id") seriesId: Int, @Path("season") seasonNumber: Int): Response<Season> suspend fun getSeason(@Path("id") seriesId: Int, @Path("season") seasonNumber: Int): Response<Season>
@GET("tv/{id}/season/{season}/account_states")
suspend fun getSeasonAccountStates(@Path("id") seriesId: Int, @Path("season") seasonNumber: Int): Response<SeasonAccountStates>
@GET("tv/{id}/season/{season}/aggregate_credits")
suspend fun getSeasonCredits(@Path("id") seriesId: Int, @Path("season") seasonNumber: Int): Response<TvCastAndCrew>
@GET("tv/{id}/season/{season}/images")
suspend fun getSeasonImages(@Path("id") seriesId: Int, @Path("season") seasonNumber: Int): Response<ImageCollection>
@GET("tv/{id}/season/{season}/videos")
suspend fun getSeasonVideos(@Path("id") seriesId: Int, @Path("season") seasonNumber: Int): Response<VideoResponse>
@GET("tv/{id}/season/{season}/watch/providers")
suspend fun getSeasonWatchProviders(@Path("id") seriesId: Int, @Path("season") seasonNumber: Int): Response<WatchProviderResponse>
} }

View File

@@ -27,9 +27,13 @@ import com.owenlejeune.tvtime.api.tmdb.api.v3.model.ReviewResponse
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.SearchResult import com.owenlejeune.tvtime.api.tmdb.api.v3.model.SearchResult
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.SearchResultMedia import com.owenlejeune.tvtime.api.tmdb.api.v3.model.SearchResultMedia
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.Season import com.owenlejeune.tvtime.api.tmdb.api.v3.model.Season
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.SeasonAccountStates
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.StatusResponse import com.owenlejeune.tvtime.api.tmdb.api.v3.model.StatusResponse
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.TmdbItem import com.owenlejeune.tvtime.api.tmdb.api.v3.model.TmdbItem
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.TvCastAndCrew
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.TvCastMember
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.TvContentRatings import com.owenlejeune.tvtime.api.tmdb.api.v3.model.TvContentRatings
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.TvCrewMember
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.Video import com.owenlejeune.tvtime.api.tmdb.api.v3.model.Video
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.VideoResponse import com.owenlejeune.tvtime.api.tmdb.api.v3.model.VideoResponse
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.WatchProviderResponse import com.owenlejeune.tvtime.api.tmdb.api.v3.model.WatchProviderResponse
@@ -69,6 +73,30 @@ class TvService: KoinComponent, DetailService, HomePageService {
val seasons: MutableMap<Int, out Set<Season>> val seasons: MutableMap<Int, out Set<Season>>
get() = _seasons get() = _seasons
private val _seasonAccountStates = Collections.synchronizedMap(mutableStateMapOf<Int, MutableMap<Int, SeasonAccountStates>>())
val seasonAccountStates: MutableMap<Int, out Map<Int, SeasonAccountStates>>
get() = _seasonAccountStates
private val _seasonCast = Collections.synchronizedMap(mutableStateMapOf<Int, MutableMap<Int, List<TvCastMember>>>())
val seasonCast: MutableMap<Int, out Map<Int, List<TvCastMember>>>
get() = _seasonCast
private val _seasonCrew = Collections.synchronizedMap(mutableStateMapOf<Int, MutableMap<Int, List<TvCrewMember>>>())
val seasonCrew: MutableMap<Int, out Map<Int, List<TvCrewMember>>>
get() = _seasonCrew
private val _seasonImages = Collections.synchronizedMap(mutableStateMapOf<Int, MutableMap<Int, ImageCollection>>())
val seasonImages: Map<Int, out Map<Int, ImageCollection>>
get() = _seasonImages
private val _seasonVideos = Collections.synchronizedMap(mutableStateMapOf<Int, MutableMap<Int, List<Video>>>())
val seasonVideos: MutableMap<Int, out Map<Int, List<Video>>>
get() = _seasonVideos
private val _seasonWatchProviders = Collections.synchronizedMap(mutableStateMapOf<Int, MutableMap<Int, WatchProviders>>())
val seasonWatchProviders: MutableMap<Int, out Map<Int, WatchProviders>>
get() = _seasonWatchProviders
val detailsLoadingState = mutableStateOf(LoadingState.INACTIVE) val detailsLoadingState = mutableStateOf(LoadingState.INACTIVE)
val imagesLoadingState = mutableStateOf(LoadingState.INACTIVE) val imagesLoadingState = mutableStateOf(LoadingState.INACTIVE)
val castCrewLoadingState = mutableStateOf(LoadingState.INACTIVE) val castCrewLoadingState = mutableStateOf(LoadingState.INACTIVE)
@@ -80,6 +108,11 @@ class TvService: KoinComponent, DetailService, HomePageService {
val externalIdsLoadingState = mutableStateOf(LoadingState.INACTIVE) val externalIdsLoadingState = mutableStateOf(LoadingState.INACTIVE)
val accountStatesLoadingState = mutableStateOf(LoadingState.INACTIVE) val accountStatesLoadingState = mutableStateOf(LoadingState.INACTIVE)
val seasonsLoadingState = mutableStateOf(LoadingState.INACTIVE) val seasonsLoadingState = mutableStateOf(LoadingState.INACTIVE)
val seasonAccountStatesLoadingState = mutableStateOf(LoadingState.INACTIVE)
val seasonCreditsLoadingState = mutableStateOf(LoadingState.INACTIVE)
val seasonImagesLoadingState = mutableStateOf(LoadingState.INACTIVE)
val seasonVideosLoadingState = mutableStateOf(LoadingState.INACTIVE)
val seasonWatchProvidersLoadingState = mutableStateOf(LoadingState.INACTIVE)
override suspend fun getById(id: Int, refreshing: Boolean) { override suspend fun getById(id: Int, refreshing: Boolean) {
loadRemoteData( loadRemoteData(
@@ -169,6 +202,15 @@ class TvService: KoinComponent, DetailService, HomePageService {
) )
} }
override suspend fun getExternalIds(id: Int, refreshing: Boolean) {
loadRemoteData(
{ service.getExternalIds(id) },
{ externalIds[id] = it },
externalIdsLoadingState,
refreshing
)
}
suspend fun getSeason(seriesId: Int, seasonId: Int, refreshing: Boolean) { suspend fun getSeason(seriesId: Int, seasonId: Int, refreshing: Boolean) {
loadRemoteData( loadRemoteData(
{ service.getSeason(seriesId, seasonId) }, { service.getSeason(seriesId, seasonId) },
@@ -186,11 +228,78 @@ class TvService: KoinComponent, DetailService, HomePageService {
) )
} }
override suspend fun getExternalIds(id: Int, refreshing: Boolean) { suspend fun getSeasonAccountStates(seriesId: Int, seasonId: Int, refreshing: Boolean) {
loadRemoteData( loadRemoteData(
{ service.getExternalIds(id) }, { service.getSeasonAccountStates(seriesId, seasonId) },
{ externalIds[id] = it }, { sas ->
externalIdsLoadingState, _seasonAccountStates
.getOrPut(seriesId) {
emptyMap<Int, SeasonAccountStates>().toMutableMap()
}[seasonId] = sas
},
seasonAccountStatesLoadingState,
refreshing
)
}
suspend fun getSeasonCredits(seriesId: Int, seasonId: Int, refreshing: Boolean) {
loadRemoteData(
{ service.getSeasonCredits(seriesId, seasonId) },
{ sc ->
_seasonCast
.getOrPut(seriesId) {
emptyMap<Int, List<TvCastMember>>().toMutableMap()
}[seasonId] = sc.cast
_seasonCrew
.getOrPut(seriesId) {
emptyMap<Int, List<TvCrewMember>>().toMutableMap()
}[seasonId] = sc.crew
},
seasonCreditsLoadingState,
refreshing
)
}
suspend fun getSeasonImages(seriesId: Int, seasonId: Int, refreshing: Boolean) {
loadRemoteData(
{ service.getSeasonImages(seriesId, seasonId) },
{ si ->
_seasonImages
.getOrPut(seriesId) {
emptyMap<Int, ImageCollection>().toMutableMap()
}[seasonId] = si
},
seasonImagesLoadingState,
refreshing
)
}
suspend fun getSeasonVideos(seriesId: Int, seasonId: Int, refreshing: Boolean) {
loadRemoteData(
{ service.getSeasonVideos(seriesId, seasonId) },
{ sv ->
_seasonVideos
.getOrPut(seriesId) {
emptyMap<Int, List<Video>>().toMutableMap()
}[seasonId] = sv.results
},
seasonVideosLoadingState,
refreshing
)
}
suspend fun getSeasonWatchProviders(seriesId: Int, seasonId: Int, refreshing: Boolean) {
loadRemoteData(
{ service.getSeasonWatchProviders(seriesId, seasonId) },
{ si ->
si.results[Locale.getDefault().country]?.let { wp ->
_seasonWatchProviders
.getOrPut(seriesId) {
emptyMap<Int, WatchProviders>().toMutableMap()
}[seasonId] = wp
}
},
seasonWatchProvidersLoadingState,
refreshing refreshing
) )
} }

View File

@@ -0,0 +1,21 @@
package com.owenlejeune.tvtime.api.tmdb.api.v3.deserializer
import com.google.gson.JsonObject
import com.owenlejeune.tvtime.api.tmdb.api.BaseDeserializer
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.SeasonAccountStatesResult
class SeasonAccountStatesResultDeserializer: BaseDeserializer<SeasonAccountStatesResult>() {
override fun processJson(obj: JsonObject): SeasonAccountStatesResult {
val id = obj.get("id").asInt
val episodeNumber = obj.get("episode_number").asInt
return try {
val isRated = obj.get("rated").asBoolean
SeasonAccountStatesResult(id, episodeNumber, isRated, -1)
} catch (e: Exception) {
val rating = obj.get("rated").asJsonObject.get("value").asInt
SeasonAccountStatesResult(id, episodeNumber, true, rating)
}
}
}

View File

@@ -5,5 +5,7 @@ import com.google.gson.annotations.SerializedName
data class Image( data class Image(
@SerializedName("file_path") val filePath: String, @SerializedName("file_path") val filePath: String,
@SerializedName("height") val height: Int, @SerializedName("height") val height: Int,
@SerializedName("width") val width: Int @SerializedName("width") val width: Int,
@SerializedName("aspect_ratio") val aspectRatio: Float? = null,
@SerializedName("iso_639_1") val language: String? = null
) )

View File

@@ -1,10 +0,0 @@
package com.owenlejeune.tvtime.api.tmdb.api.v3.model
import com.google.gson.annotations.SerializedName
class PersonImage(
@SerializedName("aspect_ratio") val aspectRatio: Float,
@SerializedName("file_path") val filePath: String,
@SerializedName("width") val width: Int,
@SerializedName("height") val height: Int
)

View File

@@ -3,5 +3,5 @@ package com.owenlejeune.tvtime.api.tmdb.api.v3.model
import com.google.gson.annotations.SerializedName import com.google.gson.annotations.SerializedName
class PersonImageCollection( class PersonImageCollection(
@SerializedName("profiles") val images: List<PersonImage> @SerializedName("profiles") val images: List<Image>
) )

View File

@@ -0,0 +1,15 @@
package com.owenlejeune.tvtime.api.tmdb.api.v3.model
import com.google.gson.annotations.SerializedName
class SeasonAccountStates(
@SerializedName("id") val id: Int,
@SerializedName("results") val results: List<SeasonAccountStatesResult>
)
class SeasonAccountStatesResult(
@SerializedName("id") val id: Int,
@SerializedName("episode_number") val episodeNumber: Int,
val isRated: Boolean,
var rating: Int
)

View File

@@ -21,12 +21,14 @@ import com.owenlejeune.tvtime.api.tmdb.api.v3.deserializer.CreditMediaDeserializ
import com.owenlejeune.tvtime.api.tmdb.api.v3.deserializer.DetailCastDeserializer import com.owenlejeune.tvtime.api.tmdb.api.v3.deserializer.DetailCastDeserializer
import com.owenlejeune.tvtime.api.tmdb.api.v3.deserializer.DetailCrewDeserializer import com.owenlejeune.tvtime.api.tmdb.api.v3.deserializer.DetailCrewDeserializer
import com.owenlejeune.tvtime.api.tmdb.api.v3.deserializer.KnownForDeserializer import com.owenlejeune.tvtime.api.tmdb.api.v3.deserializer.KnownForDeserializer
import com.owenlejeune.tvtime.api.tmdb.api.v3.deserializer.SeasonAccountStatesResultDeserializer
import com.owenlejeune.tvtime.api.tmdb.api.v3.deserializer.SortableSearchResultDeserializer import com.owenlejeune.tvtime.api.tmdb.api.v3.deserializer.SortableSearchResultDeserializer
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.AccountStates import com.owenlejeune.tvtime.api.tmdb.api.v3.model.AccountStates
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.CreditMedia import com.owenlejeune.tvtime.api.tmdb.api.v3.model.CreditMedia
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.DetailCast import com.owenlejeune.tvtime.api.tmdb.api.v3.model.DetailCast
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.DetailCrew import com.owenlejeune.tvtime.api.tmdb.api.v3.model.DetailCrew
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.KnownFor import com.owenlejeune.tvtime.api.tmdb.api.v3.model.KnownFor
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.SeasonAccountStatesResult
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.SortableSearchResult import com.owenlejeune.tvtime.api.tmdb.api.v3.model.SortableSearchResult
import com.owenlejeune.tvtime.api.tmdb.api.v4.AccountV4Service import com.owenlejeune.tvtime.api.tmdb.api.v4.AccountV4Service
import com.owenlejeune.tvtime.api.tmdb.api.v4.AuthenticationV4Service import com.owenlejeune.tvtime.api.tmdb.api.v4.AuthenticationV4Service
@@ -86,7 +88,8 @@ val networkModule = module {
DetailCast::class.java to DetailCastDeserializer(), DetailCast::class.java to DetailCastDeserializer(),
DetailCrew::class.java to DetailCrewDeserializer(), DetailCrew::class.java to DetailCrewDeserializer(),
CreditMedia::class.java to CreditMediaDeserializer(), CreditMedia::class.java to CreditMediaDeserializer(),
Date::class.java to DateTypeAdapter() Date::class.java to DateTypeAdapter(),
SeasonAccountStatesResult::class.java to SeasonAccountStatesResultDeserializer()
) )
} }

View File

@@ -2,13 +2,18 @@ package com.owenlejeune.tvtime.ui.components
import android.content.Intent import android.content.Intent
import android.net.Uri import android.net.Uri
import androidx.compose.animation.Crossfade
import androidx.compose.foundation.Image import androidx.compose.foundation.Image
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Person
import androidx.compose.material3.Card import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults import androidx.compose.material3.CardDefaults
import androidx.compose.material3.Divider import androidx.compose.material3.Divider
@@ -28,7 +33,9 @@ import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.IntSize
@@ -47,12 +54,17 @@ import com.google.accompanist.pager.rememberPagerState
import com.owenlejeune.tvtime.R import com.owenlejeune.tvtime.R
import com.owenlejeune.tvtime.api.LoadingState import com.owenlejeune.tvtime.api.LoadingState
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.Episode import com.owenlejeune.tvtime.api.tmdb.api.v3.model.Episode
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.Image
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.ImageCollection import com.owenlejeune.tvtime.api.tmdb.api.v3.model.ImageCollection
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.MovieCastMember import com.owenlejeune.tvtime.api.tmdb.api.v3.model.MovieCastMember
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.MovieCrewMember import com.owenlejeune.tvtime.api.tmdb.api.v3.model.MovieCrewMember
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.Person import com.owenlejeune.tvtime.api.tmdb.api.v3.model.Person
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.TvCastMember import com.owenlejeune.tvtime.api.tmdb.api.v3.model.TvCastMember
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.TvCrewMember import com.owenlejeune.tvtime.api.tmdb.api.v3.model.TvCrewMember
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.Video
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.WatchProviderDetails
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.WatchProviders
import com.owenlejeune.tvtime.extensions.listItems
import com.owenlejeune.tvtime.extensions.shimmerBackground import com.owenlejeune.tvtime.extensions.shimmerBackground
import com.owenlejeune.tvtime.extensions.toDp import com.owenlejeune.tvtime.extensions.toDp
import com.owenlejeune.tvtime.ui.navigation.AppNavItem import com.owenlejeune.tvtime.ui.navigation.AppNavItem
@@ -408,7 +420,8 @@ fun AdditionalDetailItem(
fun EpisodeItem( fun EpisodeItem(
episode: Episode, episode: Episode,
elevation: Dp = 10.dp, elevation: Dp = 10.dp,
maxDescriptionLines: Int = 2 maxDescriptionLines: Int = 2,
rating: Int? = null
) { ) {
Card( Card(
shape = RoundedCornerShape(10.dp), shape = RoundedCornerShape(10.dp),
@@ -478,6 +491,14 @@ fun EpisodeItem(
color = textColor, color = textColor,
maxLines = maxDescriptionLines maxLines = maxDescriptionLines
) )
rating?.let {
Text(
text = stringResource(id = R.string.your_rating, rating),
color = textColor.copy(alpha = 0.8f),
fontSize = 12.sp,
fontStyle = FontStyle.Italic
)
}
} }
} }
} }
@@ -518,3 +539,222 @@ fun CastCrewCard(
} }
) )
} }
@Composable
fun WatchProvidersCard(
modifier: Modifier = Modifier,
providers: WatchProviders
) {
Card(
modifier = modifier
.fillMaxWidth()
.wrapContentHeight(),
shape = RoundedCornerShape(10.dp),
elevation = CardDefaults.cardElevation(defaultElevation = 8.dp),
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant)
) {
Text(
text = stringResource(R.string.watch_providers_title),
style = MaterialTheme.typography.titleLarge,
modifier = Modifier.padding(start = 16.dp, top = 12.dp),
color = MaterialTheme.colorScheme.onSurfaceVariant
)
val itemsMap = mutableMapOf<Int, List<WatchProviderDetails>>().apply {
providers.flaterate?.let { put(0, it) }
providers.rent?.let { put(1, it) }
providers.buy?.let { put(2, it) }
}
val selected = remember { mutableStateOf(if (itemsMap.isEmpty()) null else itemsMap.values.first()) }
val context = LocalContext.current
PillSegmentedControl(
items = itemsMap.values.toList(),
itemLabel = { i, _ ->
when (i) {
0 -> context.getString(R.string.streaming_label)
1 -> context.getString(R.string.rent_label)
2 -> context.getString(R.string.buy_label)
else -> ""
}
},
onItemSelected = { i, _ -> selected.value = itemsMap.values.toList()[i] },
modifier = Modifier.padding(all = 8.dp)
)
Crossfade(
modifier = modifier.padding(top = 4.dp, bottom = 12.dp),
targetState = selected.value
) { value ->
WatchProviderContainer(watchProviders = value!!, link = providers.link)
}
}
}
@Composable
fun WatchProviderContainer(
watchProviders: List<WatchProviderDetails>,
link: String
) {
val context = LocalContext.current
com.google.accompanist.flowlayout.FlowRow(
modifier = Modifier.padding(horizontal = 16.dp),
mainAxisSpacing = 8.dp,
crossAxisSpacing = 4.dp
) {
watchProviders
.sortedBy { it.displayPriority }
.forEach { item ->
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(4.dp),
modifier = Modifier
.clip(RoundedCornerShape(10.dp))
.clickable {
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(link))
context.startActivity(intent)
}
) {
val url = TmdbUtils.fullLogoPath(item.logoPath)
val model = ImageRequest.Builder(LocalContext.current)
.data(url)
.diskCacheKey(url)
.networkCachePolicy(CachePolicy.ENABLED)
.memoryCachePolicy(CachePolicy.ENABLED)
.build()
AsyncImage(
model = model,
contentDescription = null,
modifier = Modifier
.size(48.dp)
.clip(RoundedCornerShape(10.dp))
)
Text(
text = item.providerName,
fontSize = 10.sp,
modifier = Modifier.width(48.dp),
maxLines = 2,
overflow = TextOverflow.Ellipsis,
textAlign = TextAlign.Center
)
}
}
}
}
@Composable
fun VideosCard(
modifier: Modifier = Modifier,
videos: List<Video>
) {
ExpandableContentCard(
modifier = modifier,
title = {
Text(
text = stringResource(id = R.string.videos_label),
style = MaterialTheme.typography.titleLarge,
modifier = Modifier.padding(start = 12.dp, top = 8.dp),
color = MaterialTheme.colorScheme.onSurfaceVariant
)
},
toggleTextColor = MaterialTheme.colorScheme.primary
) { isExpanded ->
VideoGroup(
results = videos,
type = Video.Type.TRAILER,
title = stringResource(id = Video.Type.TRAILER.stringRes)
)
if (isExpanded) {
Video.Type.values().filter { it != Video.Type.TRAILER }.forEach { type ->
VideoGroup(
results = videos,
type = type,
title = stringResource(id = type.stringRes)
)
}
}
}
}
@Composable
fun VideoGroup(
results: List<Video>,
type: Video.Type,
title: String
) {
val videos = results.filter { it.isOfficial && it.type == type }
if (videos.isNotEmpty()) {
Text(
text = title,
color = MaterialTheme.colorScheme.primary,
modifier = Modifier.padding(start = 12.dp, top = 8.dp)
)
val posterWidth = 120.dp
LazyRow(modifier = Modifier
.padding(vertical = 8.dp),
horizontalArrangement = Arrangement.spacedBy(4.dp)
) {
item {
Spacer(modifier = Modifier.width(8.dp))
}
listItems(videos) { video ->
FullScreenThumbnailVideoPlayer(
key = video.key,
title = video.name,
modifier = Modifier
.width(posterWidth)
.wrapContentHeight()
)
}
item {
Spacer(modifier = Modifier.width(8.dp))
}
}
}
}
@Composable
fun ImagesCard(
images: List<Image>,
onSeeAll: (() -> Unit)? = null
) {
ContentCard(
title = stringResource(R.string.images_title)
) {
LazyRow(
modifier = Modifier
.fillMaxWidth()
.wrapContentHeight()
.padding(vertical = 12.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
item {
Spacer(modifier = Modifier.width(8.dp))
}
items(images) { image ->
PosterItem(
width = 120.dp,
url = TmdbUtils.getFullPersonImagePath(image.filePath),
placeholder = Icons.Filled.Person,
title = ""
)
}
item {
Spacer(modifier = Modifier.width(8.dp))
}
}
onSeeAll?.let {
Text(
text = stringResource(id = R.string.expand_see_all),
color = MaterialTheme.colorScheme.onSurfaceVariant,
fontSize = 12.sp,
modifier = Modifier
.padding(start = 16.dp, bottom = 16.dp)
.clickable(onClick = onSeeAll)
)
}
}
}

View File

@@ -1,11 +1,8 @@
package com.owenlejeune.tvtime.ui.screens package com.owenlejeune.tvtime.ui.screens
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.Intent
import android.net.Uri
import android.widget.Toast import android.widget.Toast
import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.Crossfade
import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut import androidx.compose.animation.fadeOut
import androidx.compose.foundation.background import androidx.compose.foundation.background
@@ -21,7 +18,6 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height 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.width import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.lazy.LazyRow
@@ -70,7 +66,6 @@ import androidx.paging.compose.collectAsLazyPagingItems
import coil.compose.AsyncImage import coil.compose.AsyncImage
import coil.request.CachePolicy import coil.request.CachePolicy
import coil.request.ImageRequest import coil.request.ImageRequest
import com.google.accompanist.flowlayout.FlowRow
import com.google.accompanist.pager.ExperimentalPagerApi import com.google.accompanist.pager.ExperimentalPagerApi
import com.google.accompanist.pager.PagerState import com.google.accompanist.pager.PagerState
import com.google.accompanist.pager.rememberPagerState import com.google.accompanist.pager.rememberPagerState
@@ -81,15 +76,10 @@ import com.owenlejeune.tvtime.api.tmdb.api.v3.model.DetailedMovie
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.DetailedTv import com.owenlejeune.tvtime.api.tmdb.api.v3.model.DetailedTv
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.Genre import com.owenlejeune.tvtime.api.tmdb.api.v3.model.Genre
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.ImageCollection import com.owenlejeune.tvtime.api.tmdb.api.v3.model.ImageCollection
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.MovieCastMember
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.MovieCrewMember
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.Person
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.TvCastMember
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.TvCrewMember
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.Video import com.owenlejeune.tvtime.api.tmdb.api.v3.model.Video
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.WatchProviderDetails
import com.owenlejeune.tvtime.extensions.DateFormat import com.owenlejeune.tvtime.extensions.DateFormat
import com.owenlejeune.tvtime.extensions.WindowSizeClass import com.owenlejeune.tvtime.extensions.WindowSizeClass
import com.owenlejeune.tvtime.extensions.combineWith
import com.owenlejeune.tvtime.extensions.combinedOnVisibilityChange import com.owenlejeune.tvtime.extensions.combinedOnVisibilityChange
import com.owenlejeune.tvtime.extensions.format import com.owenlejeune.tvtime.extensions.format
import com.owenlejeune.tvtime.extensions.getCalendarYear import com.owenlejeune.tvtime.extensions.getCalendarYear
@@ -116,7 +106,6 @@ import com.owenlejeune.tvtime.ui.components.FullScreenThumbnailVideoPlayer
import com.owenlejeune.tvtime.ui.components.HtmlText import com.owenlejeune.tvtime.ui.components.HtmlText
import com.owenlejeune.tvtime.ui.components.ImageGalleryOverlay import com.owenlejeune.tvtime.ui.components.ImageGalleryOverlay
import com.owenlejeune.tvtime.ui.components.ListContentCard import com.owenlejeune.tvtime.ui.components.ListContentCard
import com.owenlejeune.tvtime.ui.components.PillSegmentedControl
import com.owenlejeune.tvtime.ui.components.PlaceholderDetailHeader import com.owenlejeune.tvtime.ui.components.PlaceholderDetailHeader
import com.owenlejeune.tvtime.ui.components.PlaceholderPosterItem import com.owenlejeune.tvtime.ui.components.PlaceholderPosterItem
import com.owenlejeune.tvtime.ui.components.PosterItem import com.owenlejeune.tvtime.ui.components.PosterItem
@@ -124,6 +113,8 @@ import com.owenlejeune.tvtime.ui.components.RoundedChip
import com.owenlejeune.tvtime.ui.components.RoundedTextField import com.owenlejeune.tvtime.ui.components.RoundedTextField
import com.owenlejeune.tvtime.ui.components.TVTTopAppBar import com.owenlejeune.tvtime.ui.components.TVTTopAppBar
import com.owenlejeune.tvtime.ui.components.TwoLineImageTextCard import com.owenlejeune.tvtime.ui.components.TwoLineImageTextCard
import com.owenlejeune.tvtime.ui.components.VideosCard
import com.owenlejeune.tvtime.ui.components.WatchProvidersCard
import com.owenlejeune.tvtime.ui.navigation.AppNavItem import com.owenlejeune.tvtime.ui.navigation.AppNavItem
import com.owenlejeune.tvtime.ui.theme.Typography import com.owenlejeune.tvtime.ui.theme.Typography
import com.owenlejeune.tvtime.ui.viewmodel.ApplicationViewModel import com.owenlejeune.tvtime.ui.viewmodel.ApplicationViewModel
@@ -377,7 +368,7 @@ fun MediaViewContent(
mainViewModel = mainViewModel mainViewModel = mainViewModel
) )
VideosCard( VideosArea(
itemId = itemId, itemId = itemId,
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
mainViewModel = mainViewModel, mainViewModel = mainViewModel,
@@ -386,7 +377,7 @@ fun MediaViewContent(
AdditionalDetailsCard(mediaItem = mediaItem, type = type) AdditionalDetailsCard(mediaItem = mediaItem, type = type)
WatchProvidersCard(itemId = itemId, type = type, mainViewModel = mainViewModel) WatchProvidersArea(itemId = itemId, type = type, mainViewModel = mainViewModel)
if ( if (
mediaItem?.productionCompanies?.firstOrNull { it.name == "Marvel Studios" } != null mediaItem?.productionCompanies?.firstOrNull { it.name == "Marvel Studios" } != null
@@ -891,6 +882,9 @@ private fun SeasonCard(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
modifier = Modifier modifier = Modifier
.padding(all = 12.dp) .padding(all = 12.dp)
.clickable {
appNavController.navigate(AppNavItem.DetailView.withArgs(MediaViewType.SEASON, itemId.combineWith(it.seasonNumber)))
}
) { ) {
PosterItem( PosterItem(
url = TmdbUtils.getFullPosterPath(it.posterPath), url = TmdbUtils.getFullPosterPath(it.posterPath),
@@ -992,7 +986,7 @@ fun SimilarContentCard(
} }
@Composable @Composable
fun VideosCard( fun VideosArea(
itemId: Int, itemId: Int,
type: MediaViewType, type: MediaViewType,
mainViewModel: MainViewModel, mainViewModel: MainViewModel,
@@ -1002,74 +996,13 @@ fun VideosCard(
val videos = videosMap[itemId] val videos = videosMap[itemId]
if (videos?.any { it.isOfficial } == true) { if (videos?.any { it.isOfficial } == true) {
ExpandableContentCard( VideosCard(videos = videos, modifier = modifier)
modifier = modifier,
title = {
Text(
text = stringResource(id = R.string.videos_label),
style = MaterialTheme.typography.titleLarge,
modifier = Modifier.padding(start = 12.dp, top = 8.dp),
color = MaterialTheme.colorScheme.onSurfaceVariant
)
},
toggleTextColor = MaterialTheme.colorScheme.primary
) { isExpanded ->
VideoGroup(
results = videos,
type = Video.Type.TRAILER,
title = stringResource(id = Video.Type.TRAILER.stringRes)
)
if (isExpanded) {
Video.Type.values().filter { it != Video.Type.TRAILER }.forEach { type ->
VideoGroup(
results = videos,
type = type,
title = stringResource(id = type.stringRes)
)
}
}
}
}
}
@Composable
private fun VideoGroup(results: List<Video>, type: Video.Type, title: String) {
val videos = results.filter { it.isOfficial && it.type == type }
if (videos.isNotEmpty()) {
Text(
text = title,
color = MaterialTheme.colorScheme.primary,
modifier = Modifier.padding(start = 12.dp, top = 8.dp)
)
val posterWidth = 120.dp
LazyRow(modifier = Modifier
.padding(vertical = 8.dp),
horizontalArrangement = Arrangement.spacedBy(4.dp)
) {
item {
Spacer(modifier = Modifier.width(8.dp))
}
listItems(videos) { video ->
FullScreenThumbnailVideoPlayer(
key = video.key,
title = video.name,
modifier = Modifier
.width(posterWidth)
.wrapContentHeight()
)
}
item {
Spacer(modifier = Modifier.width(8.dp))
}
}
} }
} }
@SuppressLint("AutoboxingStateValueProperty") @SuppressLint("AutoboxingStateValueProperty")
@Composable @Composable
private fun WatchProvidersCard( private fun WatchProvidersArea(
itemId: Int, itemId: Int,
type: MediaViewType, type: MediaViewType,
mainViewModel: MainViewModel, mainViewModel: MainViewModel,
@@ -1079,105 +1012,11 @@ private fun WatchProvidersCard(
val watchProviders = watchProvidersMap[itemId] val watchProviders = watchProvidersMap[itemId]
watchProviders?.let { providers -> watchProviders?.let { providers ->
if (providers.buy?.isNotEmpty() == true || providers.rent?.isNotEmpty() == true || providers.flaterate?.isNotEmpty() == true) { if (providers.buy?.isNotEmpty() == true || providers.rent?.isNotEmpty() == true || providers.flaterate?.isNotEmpty() == true) {
Card( WatchProvidersCard(providers = providers, modifier = modifier)
modifier = modifier
.fillMaxWidth()
.wrapContentHeight(),
shape = RoundedCornerShape(10.dp),
elevation = CardDefaults.cardElevation(defaultElevation = 8.dp),
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant)
) {
Text(
text = stringResource(R.string.watch_providers_title),
style = MaterialTheme.typography.titleLarge,
modifier = Modifier.padding(start = 16.dp, top = 12.dp),
color = MaterialTheme.colorScheme.onSurfaceVariant
)
val itemsMap = mutableMapOf<Int, List<WatchProviderDetails>>().apply {
providers.flaterate?.let { put(0, it) }
providers.rent?.let { put(1, it) }
providers.buy?.let { put(2, it) }
}
val selected = remember { mutableStateOf(if (itemsMap.isEmpty()) null else itemsMap.values.first()) }
val context = LocalContext.current
PillSegmentedControl(
items = itemsMap.values.toList(),
itemLabel = { i, _ ->
when (i) {
0 -> context.getString(R.string.streaming_label)
1 -> context.getString(R.string.rent_label)
2 -> context.getString(R.string.buy_label)
else -> ""
}
},
onItemSelected = { i, _ -> selected.value = itemsMap.values.toList()[i] },
modifier = Modifier.padding(all = 8.dp)
)
Crossfade(
modifier = modifier.padding(top = 4.dp, bottom = 12.dp),
targetState = selected.value
) { value ->
WatchProviderContainer(watchProviders = value!!, link = providers.link)
}
}
} }
} }
} }
@Composable
private fun WatchProviderContainer(
watchProviders: List<WatchProviderDetails>,
link: String
) {
val context = LocalContext.current
FlowRow(
modifier = Modifier.padding(horizontal = 16.dp),
mainAxisSpacing = 8.dp,
crossAxisSpacing = 4.dp
) {
watchProviders
.sortedBy { it.displayPriority }
.forEach { item ->
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(4.dp),
modifier = Modifier
.clip(RoundedCornerShape(10.dp))
.clickable {
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(link))
context.startActivity(intent)
}
) {
val url = TmdbUtils.fullLogoPath(item.logoPath)
val model = ImageRequest.Builder(LocalContext.current)
.data(url)
.diskCacheKey(url)
.networkCachePolicy(CachePolicy.ENABLED)
.memoryCachePolicy(CachePolicy.ENABLED)
.build()
AsyncImage(
model = model,
contentDescription = null,
modifier = Modifier
.size(48.dp)
.clip(RoundedCornerShape(10.dp))
)
Text(
text = item.providerName,
fontSize = 10.sp,
modifier = Modifier.width(48.dp),
maxLines = 2,
overflow = TextOverflow.Ellipsis,
textAlign = TextAlign.Center
)
}
}
}
}
@Composable @Composable
private fun NextMcuProjectCard( private fun NextMcuProjectCard(
itemId: Int, itemId: Int,

View File

@@ -33,7 +33,6 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.input.nestedscroll.nestedScroll
@@ -46,8 +45,6 @@ import androidx.navigation.NavController
import com.google.accompanist.pager.ExperimentalPagerApi import com.google.accompanist.pager.ExperimentalPagerApi
import com.owenlejeune.tvtime.R import com.owenlejeune.tvtime.R
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.DetailPerson import com.owenlejeune.tvtime.api.tmdb.api.v3.model.DetailPerson
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.DetailedMovie
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.DetailedTv
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.Image import com.owenlejeune.tvtime.api.tmdb.api.v3.model.Image
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.ImageCollection import com.owenlejeune.tvtime.api.tmdb.api.v3.model.ImageCollection
import com.owenlejeune.tvtime.extensions.DateFormat import com.owenlejeune.tvtime.extensions.DateFormat
@@ -60,6 +57,7 @@ import com.owenlejeune.tvtime.ui.components.ContentCard
import com.owenlejeune.tvtime.ui.components.DetailHeader import com.owenlejeune.tvtime.ui.components.DetailHeader
import com.owenlejeune.tvtime.ui.components.ExpandableContentCard import com.owenlejeune.tvtime.ui.components.ExpandableContentCard
import com.owenlejeune.tvtime.ui.components.ExternalIdsArea import com.owenlejeune.tvtime.ui.components.ExternalIdsArea
import com.owenlejeune.tvtime.ui.components.ImagesCard
import com.owenlejeune.tvtime.ui.components.PosterItem import com.owenlejeune.tvtime.ui.components.PosterItem
import com.owenlejeune.tvtime.ui.components.TVTTopAppBar import com.owenlejeune.tvtime.ui.components.TVTTopAppBar
import com.owenlejeune.tvtime.ui.components.TwoLineImageTextCard import com.owenlejeune.tvtime.ui.components.TwoLineImageTextCard
@@ -193,7 +191,7 @@ fun PersonDetailScreen(
AdditionalDetailsCard(id = personId, mainViewModel = mainViewModel) AdditionalDetailsCard(id = personId, mainViewModel = mainViewModel)
ImagesCard(id = personId, appNavController = appNavController) ImagesArea(id = personId, appNavController = appNavController)
} }
} }
@@ -331,7 +329,7 @@ private fun CreditsCard(
} }
@Composable @Composable
private fun ImagesCard( private fun ImagesArea(
id: Int, id: Int,
appNavController: NavController appNavController: NavController
) { ) {
@@ -339,46 +337,12 @@ private fun ImagesCard(
val imagesMap = remember { mainViewModel.peopleImagesMap } val imagesMap = remember { mainViewModel.peopleImagesMap }
val images = imagesMap[id] ?: emptyList() val images = imagesMap[id] ?: emptyList()
ContentCard( ImagesCard(images = images) {
title = stringResource(R.string.images_title) appNavController.navigate(
) { AppNavItem.GalleryView.withArgs(
LazyRow( MediaViewType.PERSON,
modifier = Modifier id
.fillMaxWidth() )
.wrapContentHeight()
.padding(vertical = 12.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
item {
Spacer(modifier = Modifier.width(8.dp))
}
items(images) { image ->
PosterItem(
width = 120.dp,
url = TmdbUtils.getFullPersonImagePath(image.filePath),
placeholder = Icons.Filled.Person,
title = ""
)
}
item {
Spacer(modifier = Modifier.width(8.dp))
}
}
Text(
text = stringResource(id = R.string.expand_see_all),
color = MaterialTheme.colorScheme.onSurfaceVariant,
fontSize = 12.sp,
modifier = Modifier
.padding(start = 16.dp, bottom = 16.dp)
.clickable {
appNavController.navigate(
AppNavItem.GalleryView.withArgs(
MediaViewType.PERSON,
id
)
)
}
) )
} }
} }

View File

@@ -35,7 +35,10 @@ import com.owenlejeune.tvtime.ui.components.CastCrewCard
import com.owenlejeune.tvtime.ui.components.ContentCard import com.owenlejeune.tvtime.ui.components.ContentCard
import com.owenlejeune.tvtime.ui.components.DetailHeader import com.owenlejeune.tvtime.ui.components.DetailHeader
import com.owenlejeune.tvtime.ui.components.EpisodeItem import com.owenlejeune.tvtime.ui.components.EpisodeItem
import com.owenlejeune.tvtime.ui.components.ImagesCard
import com.owenlejeune.tvtime.ui.components.TVTTopAppBar import com.owenlejeune.tvtime.ui.components.TVTTopAppBar
import com.owenlejeune.tvtime.ui.components.VideosCard
import com.owenlejeune.tvtime.ui.components.WatchProvidersCard
import com.owenlejeune.tvtime.ui.theme.Typography import com.owenlejeune.tvtime.ui.theme.Typography
import com.owenlejeune.tvtime.ui.viewmodel.ApplicationViewModel import com.owenlejeune.tvtime.ui.viewmodel.ApplicationViewModel
import com.owenlejeune.tvtime.ui.viewmodel.MainViewModel import com.owenlejeune.tvtime.ui.viewmodel.MainViewModel
@@ -52,6 +55,11 @@ private fun fetchData(
) { ) {
val scope = CoroutineScope(Dispatchers.IO) val scope = CoroutineScope(Dispatchers.IO)
scope.launch { mainViewModel.getSeason(seriesId, seasonNumber, force) } scope.launch { mainViewModel.getSeason(seriesId, seasonNumber, force) }
scope.launch { mainViewModel.getSeasonAccountStates(seriesId, seasonNumber, force) }
scope.launch { mainViewModel.getSeasonImages(seriesId, seasonNumber, force) }
scope.launch { mainViewModel.getSeasonVideos(seriesId, seasonNumber, force) }
scope.launch { mainViewModel.getSeasonCredits(seriesId, seasonNumber, force) }
scope.launch { mainViewModel.getSeasonWatchProviders(seriesId, seasonNumber, force) }
} }
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@@ -86,7 +94,14 @@ fun SeasonDetailsScreen(
} }
) { innerPadding -> ) { innerPadding ->
Box(modifier = Modifier.padding(innerPadding)) { Box(modifier = Modifier.padding(innerPadding)) {
SeasonContent(appNavController = appNavController, season = season) season?.let {
SeasonContent(
seriesId = seriesId,
appNavController = appNavController,
mainViewModel = mainViewModel,
season = season
)
}
} }
} }
} }
@@ -94,8 +109,10 @@ fun SeasonDetailsScreen(
@OptIn(ExperimentalPagerApi::class) @OptIn(ExperimentalPagerApi::class)
@Composable @Composable
private fun SeasonContent( private fun SeasonContent(
seriesId: Int,
appNavController: NavController, appNavController: NavController,
season: Season? mainViewModel: MainViewModel,
season: Season
) { ) {
Column( Column(
modifier = Modifier modifier = Modifier
@@ -104,7 +121,7 @@ private fun SeasonContent(
verticalArrangement = Arrangement.spacedBy(16.dp) verticalArrangement = Arrangement.spacedBy(16.dp)
) { ) {
DetailHeader( DetailHeader(
posterUrl = TmdbUtils.getFullPosterPath(season?.posterPath), posterUrl = TmdbUtils.getFullPosterPath(season.posterPath),
elevation = 0.dp, elevation = 0.dp,
expandedPosterAsBackdrop = true expandedPosterAsBackdrop = true
) )
@@ -115,7 +132,7 @@ private fun SeasonContent(
verticalArrangement = Arrangement.spacedBy(16.dp) verticalArrangement = Arrangement.spacedBy(16.dp)
) { ) {
Text( Text(
text = season?.name ?: "", text = season.name,
color = MaterialTheme.colorScheme.secondary, color = MaterialTheme.colorScheme.secondary,
style = Typography.headlineLarge, style = Typography.headlineLarge,
maxLines = 2, maxLines = 2,
@@ -123,8 +140,32 @@ private fun SeasonContent(
modifier = Modifier.fillMaxWidth() modifier = Modifier.fillMaxWidth()
) )
season?.episodes?.forEach { episode -> val accountStatesMap = remember { mainViewModel.tvSeasonAccountStates }
SeasonEpisodeItem(appNavController = appNavController, episode = episode) val accountStates = accountStatesMap[seriesId]?.get(season.seasonNumber)
season.episodes.forEach { episode ->
val rating = accountStates?.results?.find { it.id == episode.id }?.takeUnless { !it.isRated }?.rating
SeasonEpisodeItem(appNavController = appNavController, episode = episode, rating = rating)
}
val imagesMap = remember { mainViewModel.tvSeasonImages }
val images = imagesMap[seriesId]?.get(season.seasonNumber)
images?.let {
ImagesCard(images = images.posters)
}
val videosMap = remember { mainViewModel.tvSeasonVideos }
val videos = videosMap[seriesId]?.get(season.seasonNumber)
if (videos?.any { it.isOfficial } == true) {
VideosCard(videos = videos, modifier = Modifier.fillMaxWidth())
}
val watchProvidersMap = remember { mainViewModel.tvSeasonWatchProviders }
val watchProviders = watchProvidersMap[seriesId]?.get(season.seasonNumber)
watchProviders?.let { providers ->
if (providers.buy?.isNotEmpty() == true || providers.rent?.isNotEmpty() == true || providers.flaterate?.isNotEmpty() == true) {
WatchProvidersCard(providers = providers)
}
} }
} }
@@ -135,10 +176,16 @@ private fun SeasonContent(
@Composable @Composable
private fun SeasonEpisodeItem( private fun SeasonEpisodeItem(
appNavController: NavController, appNavController: NavController,
episode: Episode episode: Episode,
rating: Int?
) { ) {
ContentCard { ContentCard {
EpisodeItem(episode = episode, elevation = 0.dp, maxDescriptionLines = 5) EpisodeItem(
episode = episode,
elevation = 0.dp,
maxDescriptionLines = 5,
rating = rating
)
episode.guestStars?.let { guestStars -> episode.guestStars?.let { guestStars ->
Spacer(modifier = Modifier.height(12.dp)) Spacer(modifier = Modifier.height(12.dp))

View File

@@ -103,6 +103,12 @@ class MainViewModel: ViewModel(), KoinComponent {
val similarTv = tvService.similar val similarTv = tvService.similar
val tvAccountStates = tvService.accountStates val tvAccountStates = tvService.accountStates
val tvKeywordResults = tvService.keywordResults val tvKeywordResults = tvService.keywordResults
val tvSeasonAccountStates = tvService.seasonAccountStates
val tvSeasonCast = tvService.seasonCast
val tvSeasonCrew = tvService.seasonCrew
val tvSeasonImages = tvService.seasonImages
val tvSeasonVideos = tvService.seasonVideos
val tvSeasonWatchProviders = tvService.seasonWatchProviders
val tvDetailsLoadingState = tvService.detailsLoadingState val tvDetailsLoadingState = tvService.detailsLoadingState
val tvImagesLoadingState = tvService.imagesLoadingState val tvImagesLoadingState = tvService.imagesLoadingState
@@ -115,6 +121,11 @@ class MainViewModel: ViewModel(), KoinComponent {
val tvSeasonsLoadingState = tvService.seasonsLoadingState val tvSeasonsLoadingState = tvService.seasonsLoadingState
val tvContentRatingsLoadingState = tvService.contentRatingsLoadingState val tvContentRatingsLoadingState = tvService.contentRatingsLoadingState
val tvAccountStatesLoadingState = tvService.accountStatesLoadingState val tvAccountStatesLoadingState = tvService.accountStatesLoadingState
val tvSeasonAccountStatesLoadingState = tvService.seasonAccountStatesLoadingState
val tvSeasonCreditsLoadingState = tvService.seasonCreditsLoadingState
val tvSeasonImagesLoadingState = tvService.seasonImagesLoadingState
val tvSeasonVideosLoadingState = tvService.seasonVideosLoadingState
val tvSeasonWatchProvidersLoadingState = tvService.seasonWatchProvidersLoadingState
val popularTv by lazy { val popularTv by lazy {
createPagingFlow( createPagingFlow(
@@ -455,6 +466,39 @@ class MainViewModel: ViewModel(), KoinComponent {
} }
} }
suspend fun getSeasonAccountStates(seriesId: Int, seasonId: Int, force: Boolean = false) {
if (tvSeasonAccountStates[seriesId]?.get(seasonId) == null || force) {
tvService.getSeasonAccountStates(seriesId, seasonId, force)
}
}
suspend fun getSeasonImages(seriesId: Int, seasonId: Int, force: Boolean = false) {
if (tvSeasonImages[seriesId]?.get(seasonId) == null || force) {
tvService.getSeasonImages(seriesId, seasonId, force)
}
}
suspend fun getSeasonVideos(seriesId: Int, seasonId: Int, force: Boolean = false) {
if (tvSeasonVideos[seriesId]?.get(seasonId) == null || force) {
tvService.getSeasonVideos(seriesId, seasonId, force)
}
}
suspend fun getSeasonCredits(seriesId: Int, seasonId: Int, force: Boolean = false) {
if (tvSeasonCast[seriesId]?.get(seasonId) == null ||
tvSeasonCrew[seriesId]?.get(seasonId) == null ||
force
) {
tvService.getSeasonCredits(seriesId, seasonId, force)
}
}
suspend fun getSeasonWatchProviders(seriesId: Int, seasonId: Int, force: Boolean = false) {
if (tvSeasonWatchProviders[seriesId]?.get(seasonId) == null || force) {
tvService.getSeasonWatchProviders(seriesId, seasonId, force)
}
}
@SuppressLint("ComposableNaming") @SuppressLint("ComposableNaming")
@Composable @Composable
fun monitorDetailsLoadingRefreshing(refreshing: MutableState<Boolean>) { fun monitorDetailsLoadingRefreshing(refreshing: MutableState<Boolean>) {

View File

@@ -262,4 +262,5 @@
<string name="date_of_death">Date of death</string> <string name="date_of_death">Date of death</string>
<string name="unexpected_error">An unexpected error occurred</string> <string name="unexpected_error">An unexpected error occurred</string>
<string name="guest_stars_label">Guest stars</string> <string name="guest_stars_label">Guest stars</string>
<string name="your_rating">Your rating: %1$d/10</string>
</resources> </resources>