mirror of
https://github.com/owenlejeune/TVTime.git
synced 2025-11-08 04:32:43 -05:00
tv series season details screen
This commit is contained in:
@@ -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.ExternalIds
|
||||
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.SearchResultPerson
|
||||
import com.owenlejeune.tvtime.utils.types.TimeWindow
|
||||
@@ -27,7 +27,7 @@ class PeopleService: KoinComponent {
|
||||
val peopleMap = Collections.synchronizedMap(mutableStateMapOf<Int, DetailPerson>())
|
||||
val castMap = Collections.synchronizedMap(mutableStateMapOf<Int, List<DetailCast>>())
|
||||
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 detailsLoadingState = mutableStateOf(LoadingState.INACTIVE)
|
||||
|
||||
@@ -73,4 +73,19 @@ interface TvApi {
|
||||
|
||||
@GET("tv/{id}/season/{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>
|
||||
}
|
||||
@@ -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.SearchResultMedia
|
||||
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.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.TvCrewMember
|
||||
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.WatchProviderResponse
|
||||
@@ -69,6 +73,30 @@ class TvService: KoinComponent, DetailService, HomePageService {
|
||||
val seasons: MutableMap<Int, out Set<Season>>
|
||||
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 imagesLoadingState = mutableStateOf(LoadingState.INACTIVE)
|
||||
val castCrewLoadingState = mutableStateOf(LoadingState.INACTIVE)
|
||||
@@ -80,6 +108,11 @@ class TvService: KoinComponent, DetailService, HomePageService {
|
||||
val externalIdsLoadingState = mutableStateOf(LoadingState.INACTIVE)
|
||||
val accountStatesLoadingState = 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) {
|
||||
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) {
|
||||
loadRemoteData(
|
||||
{ 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(
|
||||
{ service.getExternalIds(id) },
|
||||
{ externalIds[id] = it },
|
||||
externalIdsLoadingState,
|
||||
{ service.getSeasonAccountStates(seriesId, seasonId) },
|
||||
{ sas ->
|
||||
_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
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -5,5 +5,7 @@ import com.google.gson.annotations.SerializedName
|
||||
data class Image(
|
||||
@SerializedName("file_path") val filePath: String,
|
||||
@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
|
||||
)
|
||||
@@ -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
|
||||
)
|
||||
@@ -3,5 +3,5 @@ package com.owenlejeune.tvtime.api.tmdb.api.v3.model
|
||||
import com.google.gson.annotations.SerializedName
|
||||
|
||||
class PersonImageCollection(
|
||||
@SerializedName("profiles") val images: List<PersonImage>
|
||||
@SerializedName("profiles") val images: List<Image>
|
||||
)
|
||||
@@ -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
|
||||
)
|
||||
@@ -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.DetailCrewDeserializer
|
||||
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.model.AccountStates
|
||||
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.DetailCrew
|
||||
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.v4.AccountV4Service
|
||||
import com.owenlejeune.tvtime.api.tmdb.api.v4.AuthenticationV4Service
|
||||
@@ -86,7 +88,8 @@ val networkModule = module {
|
||||
DetailCast::class.java to DetailCastDeserializer(),
|
||||
DetailCrew::class.java to DetailCrewDeserializer(),
|
||||
CreditMedia::class.java to CreditMediaDeserializer(),
|
||||
Date::class.java to DateTypeAdapter()
|
||||
Date::class.java to DateTypeAdapter(),
|
||||
SeasonAccountStatesResult::class.java to SeasonAccountStatesResultDeserializer()
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -2,13 +2,18 @@ package com.owenlejeune.tvtime.ui.components
|
||||
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import androidx.compose.animation.Crossfade
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
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.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Person
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CardDefaults
|
||||
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.platform.LocalContext
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
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.unit.Dp
|
||||
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.api.LoadingState
|
||||
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.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.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.toDp
|
||||
import com.owenlejeune.tvtime.ui.navigation.AppNavItem
|
||||
@@ -408,7 +420,8 @@ fun AdditionalDetailItem(
|
||||
fun EpisodeItem(
|
||||
episode: Episode,
|
||||
elevation: Dp = 10.dp,
|
||||
maxDescriptionLines: Int = 2
|
||||
maxDescriptionLines: Int = 2,
|
||||
rating: Int? = null
|
||||
) {
|
||||
Card(
|
||||
shape = RoundedCornerShape(10.dp),
|
||||
@@ -478,6 +491,14 @@ fun EpisodeItem(
|
||||
color = textColor,
|
||||
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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -517,4 +538,223 @@ 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)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,8 @@
|
||||
package com.owenlejeune.tvtime.ui.screens
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.widget.Toast
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.Crossfade
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
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.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.layout.wrapContentHeight
|
||||
import androidx.compose.foundation.lazy.LazyRow
|
||||
@@ -70,7 +66,6 @@ import androidx.paging.compose.collectAsLazyPagingItems
|
||||
import coil.compose.AsyncImage
|
||||
import coil.request.CachePolicy
|
||||
import coil.request.ImageRequest
|
||||
import com.google.accompanist.flowlayout.FlowRow
|
||||
import com.google.accompanist.pager.ExperimentalPagerApi
|
||||
import com.google.accompanist.pager.PagerState
|
||||
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.Genre
|
||||
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.WatchProviderDetails
|
||||
import com.owenlejeune.tvtime.extensions.DateFormat
|
||||
import com.owenlejeune.tvtime.extensions.WindowSizeClass
|
||||
import com.owenlejeune.tvtime.extensions.combineWith
|
||||
import com.owenlejeune.tvtime.extensions.combinedOnVisibilityChange
|
||||
import com.owenlejeune.tvtime.extensions.format
|
||||
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.ImageGalleryOverlay
|
||||
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.PlaceholderPosterItem
|
||||
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.TVTTopAppBar
|
||||
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.theme.Typography
|
||||
import com.owenlejeune.tvtime.ui.viewmodel.ApplicationViewModel
|
||||
@@ -377,7 +368,7 @@ fun MediaViewContent(
|
||||
mainViewModel = mainViewModel
|
||||
)
|
||||
|
||||
VideosCard(
|
||||
VideosArea(
|
||||
itemId = itemId,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
mainViewModel = mainViewModel,
|
||||
@@ -386,7 +377,7 @@ fun MediaViewContent(
|
||||
|
||||
AdditionalDetailsCard(mediaItem = mediaItem, type = type)
|
||||
|
||||
WatchProvidersCard(itemId = itemId, type = type, mainViewModel = mainViewModel)
|
||||
WatchProvidersArea(itemId = itemId, type = type, mainViewModel = mainViewModel)
|
||||
|
||||
if (
|
||||
mediaItem?.productionCompanies?.firstOrNull { it.name == "Marvel Studios" } != null
|
||||
@@ -891,6 +882,9 @@ private fun SeasonCard(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier
|
||||
.padding(all = 12.dp)
|
||||
.clickable {
|
||||
appNavController.navigate(AppNavItem.DetailView.withArgs(MediaViewType.SEASON, itemId.combineWith(it.seasonNumber)))
|
||||
}
|
||||
) {
|
||||
PosterItem(
|
||||
url = TmdbUtils.getFullPosterPath(it.posterPath),
|
||||
@@ -992,7 +986,7 @@ fun SimilarContentCard(
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun VideosCard(
|
||||
fun VideosArea(
|
||||
itemId: Int,
|
||||
type: MediaViewType,
|
||||
mainViewModel: MainViewModel,
|
||||
@@ -1002,74 +996,13 @@ fun VideosCard(
|
||||
val videos = videosMap[itemId]
|
||||
|
||||
if (videos?.any { it.isOfficial } == true) {
|
||||
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
|
||||
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))
|
||||
}
|
||||
}
|
||||
VideosCard(videos = videos, modifier = modifier)
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("AutoboxingStateValueProperty")
|
||||
@Composable
|
||||
private fun WatchProvidersCard(
|
||||
private fun WatchProvidersArea(
|
||||
itemId: Int,
|
||||
type: MediaViewType,
|
||||
mainViewModel: MainViewModel,
|
||||
@@ -1079,105 +1012,11 @@ private fun WatchProvidersCard(
|
||||
val watchProviders = watchProvidersMap[itemId]
|
||||
watchProviders?.let { providers ->
|
||||
if (providers.buy?.isNotEmpty() == true || providers.rent?.isNotEmpty() == true || providers.flaterate?.isNotEmpty() == true) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
WatchProvidersCard(providers = providers, modifier = modifier)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@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
|
||||
private fun NextMcuProjectCard(
|
||||
itemId: Int,
|
||||
|
||||
@@ -33,7 +33,6 @@ import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
@@ -46,8 +45,6 @@ import androidx.navigation.NavController
|
||||
import com.google.accompanist.pager.ExperimentalPagerApi
|
||||
import com.owenlejeune.tvtime.R
|
||||
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.ImageCollection
|
||||
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.ExpandableContentCard
|
||||
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.TVTTopAppBar
|
||||
import com.owenlejeune.tvtime.ui.components.TwoLineImageTextCard
|
||||
@@ -193,7 +191,7 @@ fun PersonDetailScreen(
|
||||
|
||||
AdditionalDetailsCard(id = personId, mainViewModel = mainViewModel)
|
||||
|
||||
ImagesCard(id = personId, appNavController = appNavController)
|
||||
ImagesArea(id = personId, appNavController = appNavController)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -331,7 +329,7 @@ private fun CreditsCard(
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ImagesCard(
|
||||
private fun ImagesArea(
|
||||
id: Int,
|
||||
appNavController: NavController
|
||||
) {
|
||||
@@ -339,46 +337,12 @@ private fun ImagesCard(
|
||||
val imagesMap = remember { mainViewModel.peopleImagesMap }
|
||||
val images = imagesMap[id] ?: emptyList()
|
||||
|
||||
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))
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
)
|
||||
)
|
||||
}
|
||||
ImagesCard(images = images) {
|
||||
appNavController.navigate(
|
||||
AppNavItem.GalleryView.withArgs(
|
||||
MediaViewType.PERSON,
|
||||
id
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -35,7 +35,10 @@ import com.owenlejeune.tvtime.ui.components.CastCrewCard
|
||||
import com.owenlejeune.tvtime.ui.components.ContentCard
|
||||
import com.owenlejeune.tvtime.ui.components.DetailHeader
|
||||
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.VideosCard
|
||||
import com.owenlejeune.tvtime.ui.components.WatchProvidersCard
|
||||
import com.owenlejeune.tvtime.ui.theme.Typography
|
||||
import com.owenlejeune.tvtime.ui.viewmodel.ApplicationViewModel
|
||||
import com.owenlejeune.tvtime.ui.viewmodel.MainViewModel
|
||||
@@ -52,6 +55,11 @@ private fun fetchData(
|
||||
) {
|
||||
val scope = CoroutineScope(Dispatchers.IO)
|
||||
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)
|
||||
@@ -86,7 +94,14 @@ fun SeasonDetailsScreen(
|
||||
}
|
||||
) { 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)
|
||||
@Composable
|
||||
private fun SeasonContent(
|
||||
seriesId: Int,
|
||||
appNavController: NavController,
|
||||
season: Season?
|
||||
mainViewModel: MainViewModel,
|
||||
season: Season
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
@@ -104,7 +121,7 @@ private fun SeasonContent(
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
DetailHeader(
|
||||
posterUrl = TmdbUtils.getFullPosterPath(season?.posterPath),
|
||||
posterUrl = TmdbUtils.getFullPosterPath(season.posterPath),
|
||||
elevation = 0.dp,
|
||||
expandedPosterAsBackdrop = true
|
||||
)
|
||||
@@ -115,7 +132,7 @@ private fun SeasonContent(
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
Text(
|
||||
text = season?.name ?: "",
|
||||
text = season.name,
|
||||
color = MaterialTheme.colorScheme.secondary,
|
||||
style = Typography.headlineLarge,
|
||||
maxLines = 2,
|
||||
@@ -123,8 +140,32 @@ private fun SeasonContent(
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
|
||||
season?.episodes?.forEach { episode ->
|
||||
SeasonEpisodeItem(appNavController = appNavController, episode = episode)
|
||||
val accountStatesMap = remember { mainViewModel.tvSeasonAccountStates }
|
||||
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
|
||||
private fun SeasonEpisodeItem(
|
||||
appNavController: NavController,
|
||||
episode: Episode
|
||||
episode: Episode,
|
||||
rating: Int?
|
||||
) {
|
||||
ContentCard {
|
||||
EpisodeItem(episode = episode, elevation = 0.dp, maxDescriptionLines = 5)
|
||||
EpisodeItem(
|
||||
episode = episode,
|
||||
elevation = 0.dp,
|
||||
maxDescriptionLines = 5,
|
||||
rating = rating
|
||||
)
|
||||
|
||||
episode.guestStars?.let { guestStars ->
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
|
||||
@@ -103,6 +103,12 @@ class MainViewModel: ViewModel(), KoinComponent {
|
||||
val similarTv = tvService.similar
|
||||
val tvAccountStates = tvService.accountStates
|
||||
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 tvImagesLoadingState = tvService.imagesLoadingState
|
||||
@@ -115,6 +121,11 @@ class MainViewModel: ViewModel(), KoinComponent {
|
||||
val tvSeasonsLoadingState = tvService.seasonsLoadingState
|
||||
val tvContentRatingsLoadingState = tvService.contentRatingsLoadingState
|
||||
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 {
|
||||
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")
|
||||
@Composable
|
||||
fun monitorDetailsLoadingRefreshing(refreshing: MutableState<Boolean>) {
|
||||
|
||||
@@ -262,4 +262,5 @@
|
||||
<string name="date_of_death">Date of death</string>
|
||||
<string name="unexpected_error">An unexpected error occurred</string>
|
||||
<string name="guest_stars_label">Guest stars</string>
|
||||
<string name="your_rating">Your rating: %1$d/10</string>
|
||||
</resources>
|
||||
Reference in New Issue
Block a user