episode details screen

This commit is contained in:
Owen LeJeune
2023-07-31 16:22:01 -04:00
parent 314e4ca964
commit a9b0a62bd8
19 changed files with 536 additions and 99 deletions

View File

@@ -1,7 +1,6 @@
package com.owenlejeune.tvtime.api.tmdb.api.v3 package com.owenlejeune.tvtime.api.tmdb.api.v3
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.* import com.owenlejeune.tvtime.api.tmdb.api.v3.model.*
import com.owenlejeune.tvtime.utils.types.TimeWindow
import retrofit2.Response import retrofit2.Response
import retrofit2.http.* import retrofit2.http.*
@@ -89,4 +88,26 @@ interface TvApi {
@GET("tv/{id}/season/{season}/watch/providers") @GET("tv/{id}/season/{season}/watch/providers")
suspend fun getSeasonWatchProviders(@Path("id") seriesId: Int, @Path("season") seasonNumber: Int): Response<WatchProviderResponse> suspend fun getSeasonWatchProviders(@Path("id") seriesId: Int, @Path("season") seasonNumber: Int): Response<WatchProviderResponse>
@GET("tv/{id}/season/{season}/episode/{episode}")
suspend fun getEpisodeDetails(@Path("id") seriesId: Int, @Path("season") seasonNumber: Int, @Path("episode") episodeNumber: Int): Response<Episode>
@GET("tv/{id}/season/{season}/episode/{episode}/account_states")
suspend fun getEpisodeAccountStates(@Path("id") seriesId: Int, @Path("season") seasonNumber: Int, @Path("episode") episodeNumber: Int): Response<EpisodeAccountState>
@GET("tv/{id}/season/{season}/episode/{episode}/credits")
suspend fun getEpisodeCredits(@Path("id") seriesId: Int, @Path("season") seasonNumber: Int, @Path("episode") episodeNumber: Int): Response<EpisodeCastAndCrew>
@GET("tv/{id}/season/{season}/episode/{episode}/images")
suspend fun getEpisodeImages(@Path("id") seriesId: Int, @Path("season") seasonNumber: Int, @Path("episode") episodeNumber: Int): Response<EpisodeImageCollection>
@FormUrlEncoded
@POST("tv/{id}/season/{season}/episode/{episode}/rating")
suspend fun postTvEpisodeRatingAsUser(
@Path("id") id: Int,
@Path("season") seasonNumber: Int,
@Path("episode") episodeNumber: Int,
@Query("session_id") sessionId: String,
@Field("value") rating: Float
): Response<StatusResponse>
} }

View File

@@ -10,6 +10,11 @@ import com.owenlejeune.tvtime.api.tmdb.api.v3.model.AccountStates
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.CastMember import com.owenlejeune.tvtime.api.tmdb.api.v3.model.CastMember
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.CrewMember import com.owenlejeune.tvtime.api.tmdb.api.v3.model.CrewMember
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.Episode
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.EpisodeAccountState
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.EpisodeCastMember
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.EpisodeCrewMember
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.EpisodeImageCollection
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.HomePageResponse import com.owenlejeune.tvtime.api.tmdb.api.v3.model.HomePageResponse
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.ImageCollection import com.owenlejeune.tvtime.api.tmdb.api.v3.model.ImageCollection
@@ -25,6 +30,7 @@ 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.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.WatchProviders import com.owenlejeune.tvtime.api.tmdb.api.v3.model.WatchProviders
import com.owenlejeune.tvtime.extensions.createEpisodeKey
import com.owenlejeune.tvtime.utils.SessionManager import com.owenlejeune.tvtime.utils.SessionManager
import com.owenlejeune.tvtime.utils.types.TimeWindow import com.owenlejeune.tvtime.utils.types.TimeWindow
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
@@ -84,6 +90,30 @@ class TvService: KoinComponent, DetailService, HomePageService {
val seasonWatchProviders: MutableMap<Int, out Map<Int, WatchProviders>> val seasonWatchProviders: MutableMap<Int, out Map<Int, WatchProviders>>
get() = _seasonWatchProviders get() = _seasonWatchProviders
private val _episodesMap = Collections.synchronizedMap(mutableStateMapOf<String, Episode>())
val episodesMap: Map<String, Episode>
get() = _episodesMap
private val _episodeAccountStates = Collections.synchronizedMap(mutableStateMapOf<String, EpisodeAccountState>())
val episodeAccountStates: Map<String, EpisodeAccountState>
get() = _episodeAccountStates
private val _episodeCast = Collections.synchronizedMap(mutableStateMapOf<String, List<EpisodeCastMember>>())
val episodeCast: Map<String, List<EpisodeCastMember>>
get() = _episodeCast
private val _episodeCrew = Collections.synchronizedMap(mutableStateMapOf<String, List<EpisodeCrewMember>>())
val episodeCrew: Map<String, List<EpisodeCrewMember>>
get() = _episodeCrew
private val _episodeGuestStars = Collections.synchronizedMap(mutableStateMapOf<String, List<EpisodeCastMember>>())
val episodeGuestStars: Map<String, List<EpisodeCastMember>>
get() = _episodeGuestStars
private val _episodeImages = Collections.synchronizedMap(mutableStateMapOf<String, EpisodeImageCollection>())
val episodeImages: Map<String, EpisodeImageCollection>
get() = _episodeImages
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)
@@ -100,6 +130,10 @@ class TvService: KoinComponent, DetailService, HomePageService {
val seasonImagesLoadingState = mutableStateOf(LoadingState.INACTIVE) val seasonImagesLoadingState = mutableStateOf(LoadingState.INACTIVE)
val seasonVideosLoadingState = mutableStateOf(LoadingState.INACTIVE) val seasonVideosLoadingState = mutableStateOf(LoadingState.INACTIVE)
val seasonWatchProvidersLoadingState = mutableStateOf(LoadingState.INACTIVE) val seasonWatchProvidersLoadingState = mutableStateOf(LoadingState.INACTIVE)
val episodeLoadingState = mutableStateOf(LoadingState.INACTIVE)
val episodeAccountStateLoadingState = mutableStateOf(LoadingState.INACTIVE)
val episodeCreditsLoadingState = mutableStateOf(LoadingState.INACTIVE)
val episodeImagesLoadingState = mutableStateOf(LoadingState.INACTIVE)
val isPopularTvLoading = mutableStateOf(false) val isPopularTvLoading = mutableStateOf(false)
val isTopRatedTvLoading = mutableStateOf(false) val isTopRatedTvLoading = mutableStateOf(false)
@@ -284,8 +318,8 @@ class TvService: KoinComponent, DetailService, HomePageService {
suspend fun getSeasonWatchProviders(seriesId: Int, seasonId: Int, refreshing: Boolean) { suspend fun getSeasonWatchProviders(seriesId: Int, seasonId: Int, refreshing: Boolean) {
loadRemoteData( loadRemoteData(
{ service.getSeasonWatchProviders(seriesId, seasonId) }, { service.getSeasonWatchProviders(seriesId, seasonId) },
{ si -> { swp ->
si.results[Locale.getDefault().country]?.let { wp -> swp.results[Locale.getDefault().country]?.let { wp ->
_seasonWatchProviders _seasonWatchProviders
.getOrPut(seriesId) { .getOrPut(seriesId) {
emptyMap<Int, WatchProviders>().toMutableMap() emptyMap<Int, WatchProviders>().toMutableMap()
@@ -297,6 +331,91 @@ class TvService: KoinComponent, DetailService, HomePageService {
) )
} }
suspend fun getEpisode(
seriesId: Int,
seasonId: Int,
episodeId: Int,
refreshing: Boolean
) {
loadRemoteData(
{ service.getEpisodeDetails(seriesId, seasonId, episodeId) },
{ episode ->
val key = createEpisodeKey(seriesId, seasonId, episodeId)
_episodesMap[key] = episode
},
episodeLoadingState,
refreshing
)
}
suspend fun getEpisodeAccountStates(
seriesId: Int,
seasonId: Int,
episodeId: Int,
refreshing: Boolean
) {
loadRemoteData(
{ service.getEpisodeAccountStates(seriesId, seasonId, episodeId) },
{ eas ->
val key = createEpisodeKey(seriesId, seasonId, episodeId)
_episodeAccountStates[key] = eas
},
episodeAccountStateLoadingState,
refreshing
)
}
suspend fun getEpisodeCredits(
seriesId: Int,
seasonId: Int,
episodeId: Int,
refreshing: Boolean
) {
loadRemoteData(
{ service.getEpisodeCredits(seriesId, seasonId, episodeId) },
{ ec ->
val key = createEpisodeKey(seriesId, seasonId, episodeId)
_episodeCast[key] = ec.cast
_episodeCrew[key] = ec.crew
_episodeGuestStars[key] = ec.guestStars
},
episodeCreditsLoadingState,
refreshing
)
}
suspend fun getEpisodeImages(
seriesId: Int,
seasonId: Int,
episodeId: Int,
refreshing: Boolean
) {
loadRemoteData(
{ service.getEpisodeImages(seriesId, seasonId, episodeId) },
{ ei ->
val key = createEpisodeKey(seriesId, seasonId, episodeId)
_episodeImages[key] = ei
},
episodeImagesLoadingState,
refreshing
)
}
suspend fun postEpisodeRating(
seriesId: Int,
seasonId: Int,
episodeId: Int,
rating: Float
) {
val session = SessionManager.currentSession.value ?: throw Exception("Session must not be null")
val response = service.postTvEpisodeRatingAsUser(seriesId, seasonId, episodeId, session.sessionId, rating)
if (response.isSuccessful) {
Log.d(TAG, "Successfully posted rating")
} else {
Log.w(TAG, "Issue posting rating")
}
}
override suspend fun postRating(id: Int, rating: Float) { override suspend fun postRating(id: Int, rating: Float) {
val session = SessionManager.currentSession.value ?: throw Exception("Session must not be null") val session = SessionManager.currentSession.value ?: throw Exception("Session must not be null")
val response = service.postTvRatingAsUser(id, session.sessionId, rating) val response = service.postTvRatingAsUser(id, session.sessionId, rating)

View File

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

View File

@@ -10,3 +10,9 @@ abstract class CastAndCrew<C, R>(
class TvCastAndCrew(cast: List<TvCastMember>, crew: List<TvCrewMember>): CastAndCrew<TvCastMember, TvCrewMember>(cast, crew) class TvCastAndCrew(cast: List<TvCastMember>, crew: List<TvCrewMember>): CastAndCrew<TvCastMember, TvCrewMember>(cast, crew)
class MovieCastAndCrew(cast: List<MovieCastMember>, crew: List<MovieCrewMember>): CastAndCrew<MovieCastMember, MovieCrewMember>(cast, crew) class MovieCastAndCrew(cast: List<MovieCastMember>, crew: List<MovieCrewMember>): CastAndCrew<MovieCastMember, MovieCrewMember>(cast, crew)
class EpisodeCastAndCrew(
cast: List<EpisodeCastMember>,
crew: List<EpisodeCrewMember>,
@SerializedName("guest_stars") val guestStars: List<EpisodeCastMember>
): CastAndCrew<EpisodeCastMember, EpisodeCrewMember>(cast, crew)

View File

@@ -36,7 +36,8 @@ class EpisodeCastMember(
originalName: String, originalName: String,
popularity: Float, popularity: Float,
order: Int, order: Int,
@SerializedName("credit_id") val creditId: String @SerializedName("credit_id") val creditId: String,
@SerializedName("character") val character: String
): CastMember(id, name, gender, profilePath, isAdult, knownForDepartment, originalName, popularity, order) ): CastMember(id, name, gender, profilePath, isAdult, knownForDepartment, originalName, popularity, order)
class TvCastMember( class TvCastMember(
@@ -90,7 +91,8 @@ class EpisodeCrewMember(
originalName: String, originalName: String,
popularity: Float, popularity: Float,
department: String, department: String,
@SerializedName("credit_id") val creditId: String @SerializedName("credit_id") val creditId: String,
@SerializedName("job") val job: String
): CrewMember(id, name, gender, profilePath, isAdult, knownForDepartment, originalName, popularity, department) ): CrewMember(id, name, gender, profilePath, isAdult, knownForDepartment, originalName, popularity, department)
class TvCrewMember( class TvCrewMember(

View File

@@ -0,0 +1,18 @@
package com.owenlejeune.tvtime.api.tmdb.api.v3.model
import com.google.gson.annotations.SerializedName
data class EpisodeImageCollection(
@SerializedName("id") val id: Int,
@SerializedName("stills") val stills: List<EpisodeStill>
)
data class EpisodeStill(
@SerializedName("aspect_ratio") val aspectRation: Float,
@SerializedName("height") val height: Int,
@SerializedName("iso_639_1") val language: String?,
@SerializedName("file_path") val stillPath: String,
@SerializedName("vote_average") val voteAverage: Float,
@SerializedName("vote_count") val voteCount: Int,
@SerializedName("width") val width: Int
)

View File

@@ -5,7 +5,7 @@ import com.owenlejeune.tvtime.utils.types.Gender
open class Person( open class Person(
@SerializedName("id") val id: Int, @SerializedName("id") val id: Int,
@SerializedName("name") val name: String, @SerializedName("name", alternate = ["title"]) val name: String,
@SerializedName("gender") val gender: Gender, @SerializedName("gender") val gender: Gender,
@SerializedName("profile_path") val profilePath: String? @SerializedName("profile_path") val profilePath: String?
) )

View File

@@ -4,12 +4,12 @@ import com.google.gson.annotations.SerializedName
class SeasonAccountStates( class SeasonAccountStates(
@SerializedName("id") val id: Int, @SerializedName("id") val id: Int,
@SerializedName("results") val results: List<SeasonAccountStatesResult> @SerializedName("results") val results: List<EpisodeAccountState>
) )
class SeasonAccountStatesResult( class EpisodeAccountState(
@SerializedName("id") val id: Int, @SerializedName("id") val id: Int,
@SerializedName("episode_number") val episodeNumber: Int, @SerializedName("episode_number") val episodeNumber: Int?,
val isRated: Boolean, val isRated: Boolean,
var rating: Int var rating: Int
) )

View File

@@ -1,8 +1,6 @@
package com.owenlejeune.tvtime.di.modules package com.owenlejeune.tvtime.di.modules
import com.google.gson.GsonBuilder import com.google.gson.GsonBuilder
import com.google.gson.JsonDeserializer
import com.google.gson.TypeAdapter
import com.owenlejeune.tvtime.BuildConfig import com.owenlejeune.tvtime.BuildConfig
import com.owenlejeune.tvtime.api.* import com.owenlejeune.tvtime.api.*
import com.owenlejeune.tvtime.api.nextmcu.NextMCUClient import com.owenlejeune.tvtime.api.nextmcu.NextMCUClient
@@ -28,7 +26,7 @@ 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.EpisodeAccountState
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
@@ -37,7 +35,6 @@ import com.owenlejeune.tvtime.api.tmdb.api.v4.deserializer.ListItemDeserializer
import com.owenlejeune.tvtime.api.tmdb.api.v4.model.ListItem import com.owenlejeune.tvtime.api.tmdb.api.v4.model.ListItem
import com.owenlejeune.tvtime.preferences.AppPreferences import com.owenlejeune.tvtime.preferences.AppPreferences
import com.owenlejeune.tvtime.ui.viewmodel.ConfigurationViewModel import com.owenlejeune.tvtime.ui.viewmodel.ConfigurationViewModel
import com.owenlejeune.tvtime.ui.viewmodel.SettingsViewModel
import com.owenlejeune.tvtime.utils.NetworkConnectivityService import com.owenlejeune.tvtime.utils.NetworkConnectivityService
import com.owenlejeune.tvtime.utils.NetworkConnectivityServiceImpl import com.owenlejeune.tvtime.utils.NetworkConnectivityServiceImpl
import com.owenlejeune.tvtime.utils.ResourceUtils import com.owenlejeune.tvtime.utils.ResourceUtils
@@ -89,7 +86,7 @@ val networkModule = module {
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() EpisodeAccountState::class.java to SeasonAccountStatesResultDeserializer()
) )
} }

View File

@@ -13,3 +13,6 @@ fun <T> anyOf(vararg items: T, predicate: (T) -> Boolean): Boolean = items.any(p
fun <T: Any> T.isIn(vararg items: T): Boolean = items.any { it == this } fun <T: Any> T.isIn(vararg items: T): Boolean = items.any { it == this }
fun <T> pairOf(a: T, b: T) = Pair(a, b) fun <T> pairOf(a: T, b: T) = Pair(a, b)
fun createEpisodeKey(seriesId: Int, seasonNumber: Int, episodeNumber: Int): String
= listOf(seriesId, seasonNumber, episodeNumber).joinToString("_")

View File

@@ -54,6 +54,8 @@ 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.EpisodeCastMember
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.EpisodeCrewMember
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.api.tmdb.api.v3.model.MovieCastMember import com.owenlejeune.tvtime.api.tmdb.api.v3.model.MovieCastMember
@@ -64,6 +66,7 @@ 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.api.tmdb.api.v3.model.WatchProviderDetails
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.WatchProviders import com.owenlejeune.tvtime.api.tmdb.api.v3.model.WatchProviders
import com.owenlejeune.tvtime.extensions.combineWith
import com.owenlejeune.tvtime.extensions.listItems 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
@@ -147,13 +150,15 @@ fun DetailHeader(
horizontalArrangement = Arrangement.spacedBy(20.dp), horizontalArrangement = Arrangement.spacedBy(20.dp),
verticalAlignment = Alignment.Bottom verticalAlignment = Alignment.Bottom
) { ) {
PosterItem( posterUrl?.let {
url = posterUrl, PosterItem(
title = posterContentDescription, url = posterUrl,
elevation = elevation, title = posterContentDescription,
overrideShowTitle = false, elevation = elevation,
enabled = false overrideShowTitle = false,
) enabled = false
)
}
rating?.let { rating?.let {
if (it > 0f) { if (it > 0f) {
@@ -418,7 +423,9 @@ fun AdditionalDetailItem(
@Composable @Composable
fun EpisodeItem( fun EpisodeItem(
seriesId: Int,
episode: Episode, episode: Episode,
appNavController: NavController,
elevation: Dp = 10.dp, elevation: Dp = 10.dp,
maxDescriptionLines: Int = 2, maxDescriptionLines: Int = 2,
rating: Int? = null rating: Int? = null
@@ -430,7 +437,10 @@ fun EpisodeItem(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.clickable { .clickable {
val codedId = seriesId.combineWith(episode.seasonNumber).combineWith(episode.episodeNumber)
appNavController.navigate(
AppNavItem.DetailView.withArgs(MediaViewType.EPISODE, codedId)
)
} }
) { ) {
Box { Box {
@@ -527,6 +537,12 @@ fun CastCrewCard(
val epsCount = person.totalEpisodeCount val epsCount = person.totalEpisodeCount
"$roles ($epsCount Eps.)" "$roles ($epsCount Eps.)"
} }
is EpisodeCastMember -> {
person.character
}
is EpisodeCrewMember -> {
person.job
}
else -> null else -> null
}, },
imageUrl = TmdbUtils.getFullPersonImagePath(person), imageUrl = TmdbUtils.getFullPersonImagePath(person),
@@ -584,7 +600,8 @@ fun WatchProvidersCard(
Crossfade( Crossfade(
modifier = modifier.padding(top = 4.dp, bottom = 12.dp), modifier = modifier.padding(top = 4.dp, bottom = 12.dp),
targetState = selected.value targetState = selected.value,
label = ""
) { value -> ) { value ->
WatchProviderContainer(watchProviders = value!!, link = providers.link) WatchProviderContainer(watchProviders = value!!, link = providers.link)
} }
@@ -758,3 +775,65 @@ fun ImagesCard(
} }
} }
} }
@Composable
fun CastCard(
title: String,
isLoading: Boolean,
cast: List<Person>?,
appNavController: NavController,
onSeeMore: (() -> Unit)? = null,
modifier: Modifier = Modifier,
backgroundColor: Color = MaterialTheme.colorScheme.primary,
textColor: Color = MaterialTheme.colorScheme.background
) {
ContentCard(
modifier = modifier,
title = title,
backgroundColor = backgroundColor,
textColor = textColor
) {
Column {
LazyRow(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 16.dp)
) {
item { Spacer(modifier = Modifier.width(8.dp)) }
if (isLoading) {
items(5) {
PlaceholderPosterItem()
}
} else {
items(cast?.size ?: 0) { i ->
cast?.get(i)?.let {
CastCrewCard(appNavController = appNavController, person = it)
}
}
}
item { Spacer(modifier = Modifier.width(8.dp)) }
}
onSeeMore?.let {
if (isLoading) {
Text(
text = "",
modifier = Modifier
.padding(start = 12.dp, bottom = 12.dp)
.width(80.dp)
.shimmerBackground(RoundedCornerShape(10.dp))
)
} else {
Text(
text = stringResource(R.string.see_all_cast_and_crew),
fontSize = 12.sp,
color = MaterialTheme.colorScheme.inversePrimary,
modifier = Modifier
.padding(start = 12.dp, bottom = 12.dp)
.clickable(onClick = onSeeMore)
)
}
}
}
}
}

View File

@@ -22,6 +22,7 @@ import com.owenlejeune.tvtime.preferences.AppPreferences
import com.owenlejeune.tvtime.ui.screens.AboutScreen import com.owenlejeune.tvtime.ui.screens.AboutScreen
import com.owenlejeune.tvtime.ui.screens.AccountScreen import com.owenlejeune.tvtime.ui.screens.AccountScreen
import com.owenlejeune.tvtime.ui.screens.CastCrewListScreen import com.owenlejeune.tvtime.ui.screens.CastCrewListScreen
import com.owenlejeune.tvtime.ui.screens.EpisodeDetailsScreen
import com.owenlejeune.tvtime.ui.screens.GalleryView import com.owenlejeune.tvtime.ui.screens.GalleryView
import com.owenlejeune.tvtime.ui.screens.HomeScreen import com.owenlejeune.tvtime.ui.screens.HomeScreen
import com.owenlejeune.tvtime.ui.screens.KeywordResultsScreen import com.owenlejeune.tvtime.ui.screens.KeywordResultsScreen
@@ -129,6 +130,12 @@ fun AppNavigationHost(
codedId = id codedId = id
) )
} }
MediaViewType.EPISODE -> {
EpisodeDetailsScreen(
appNavController = appNavController,
codedId = id
)
}
else -> { else -> {
appNavController.popBackStack() appNavController.popBackStack()
Toast.makeText(LocalContext.current, stringResource(R.string.unexpected_error), Toast.LENGTH_SHORT).show() Toast.makeText(LocalContext.current, stringResource(R.string.unexpected_error), Toast.LENGTH_SHORT).show()

View File

@@ -0,0 +1,180 @@
package com.owenlejeune.tvtime.ui.screens
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavController
import com.google.accompanist.pager.ExperimentalPagerApi
import com.owenlejeune.tvtime.R
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.Episode
import com.owenlejeune.tvtime.extensions.createEpisodeKey
import com.owenlejeune.tvtime.extensions.toCompositeParts
import com.owenlejeune.tvtime.ui.components.BackButton
import com.owenlejeune.tvtime.ui.components.CastCard
import com.owenlejeune.tvtime.ui.components.DetailHeader
import com.owenlejeune.tvtime.ui.components.TVTTopAppBar
import com.owenlejeune.tvtime.ui.theme.Typography
import com.owenlejeune.tvtime.ui.viewmodel.ApplicationViewModel
import com.owenlejeune.tvtime.ui.viewmodel.MainViewModel
import com.owenlejeune.tvtime.utils.SessionManager
import com.owenlejeune.tvtime.utils.TmdbUtils
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
private fun fetchData(
mainViewModel: MainViewModel,
seriesId: Int,
seasonNumber: Int,
episodeNumber: Int,
force: Boolean = false
) {
val scope = CoroutineScope(Dispatchers.IO)
scope.launch { mainViewModel.getEpisode(seriesId, seasonNumber, episodeNumber, force) }
scope.launch { mainViewModel.getEpisodeCredits(seriesId, seasonNumber, episodeNumber, force) }
scope.launch { mainViewModel.getEpisodeImages(seriesId, seasonNumber, episodeNumber, force) }
if (SessionManager.isLoggedIn) {
scope.launch { mainViewModel.getEpisodeAccountStates(seriesId, seasonNumber, episodeNumber, force) }
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun EpisodeDetailsScreen(
appNavController: NavController,
codedId: Int
) {
val mainViewModel = viewModel<MainViewModel>()
val applicationViewModel = viewModel<ApplicationViewModel>()
applicationViewModel.statusBarColor.value = MaterialTheme.colorScheme.background
applicationViewModel.navigationBarColor.value = MaterialTheme.colorScheme.background
val (a, b) = codedId.toCompositeParts()
val episodeNumber = minOf(a, b)
val (c, d) = maxOf(a, b).toCompositeParts()
val seasonNumber = minOf(c, d)
val seriesId = maxOf(c, d)
LaunchedEffect(Unit) {
fetchData(mainViewModel, seriesId, seasonNumber, episodeNumber)
}
val episodeKey = createEpisodeKey(seriesId, seasonNumber, episodeNumber)
val episodesMap = remember { mainViewModel.tvEpisodes }
val episode = episodesMap[episodeKey]
Scaffold(
topBar = {
TVTTopAppBar(
title = { },
appNavController = appNavController,
navigationIcon = { BackButton(navController = appNavController) }
)
}
) { innerPadding ->
Box(modifier = Modifier.padding(innerPadding)) {
episode?.let {
EpisodeContent(
seriesId = seriesId,
episodeKey = episodeKey,
episode = episode,
appNavController = appNavController,
mainViewModel = mainViewModel
)
}
}
}
}
@OptIn(ExperimentalPagerApi::class)
@Composable
private fun EpisodeContent(
seriesId: Int,
episodeKey: String,
episode: Episode,
appNavController: NavController,
mainViewModel: MainViewModel
) {
Column(
modifier = Modifier
.background(color = MaterialTheme.colorScheme.background)
.verticalScroll(state = rememberScrollState()),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
DetailHeader(
backdropUrl = TmdbUtils.getFullEpisodeStillPath(episode.stillPath)
)
Column(
verticalArrangement = Arrangement.spacedBy(16.dp),
modifier = Modifier.padding(horizontal = 16.dp)
) {
Text(
text = episode.name,
color = MaterialTheme.colorScheme.secondary,
style = Typography.headlineLarge,
maxLines = 2,
overflow = TextOverflow.Ellipsis
)
TmdbUtils.convertEpisodeDate(episode.airDate)?.let {
Text(
text = it,
fontSize = 14.sp,
color = MaterialTheme.colorScheme.onBackground
)
}
val castMap = remember { mainViewModel.tvEpisodeCast }
val cast = castMap[episodeKey]
cast?.let {
CastCard(
title = stringResource(R.string.cast_label),
isLoading = false,
cast = cast,
appNavController = appNavController
)
}
val guestStarsMap = remember { mainViewModel.tvEpisodeGuestStars }
val guestStars = guestStarsMap[episodeKey]
guestStars?.let {
CastCard(
title = stringResource(id = R.string.guest_stars_label),
isLoading = false,
cast = guestStars,
appNavController = appNavController
)
}
val crewMap = remember { mainViewModel.tvEpisodeCrew }
val crew = crewMap[episodeKey]
crew?.let {
CastCard(
title = stringResource(id = R.string.crew_label),
isLoading = false,
cast = crew,
appNavController = appNavController
)
}
}
}
}

View File

@@ -76,7 +76,6 @@ 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.Video
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.combineWith
@@ -85,13 +84,13 @@ import com.owenlejeune.tvtime.extensions.format
import com.owenlejeune.tvtime.extensions.getCalendarYear import com.owenlejeune.tvtime.extensions.getCalendarYear
import com.owenlejeune.tvtime.extensions.isIn import com.owenlejeune.tvtime.extensions.isIn
import com.owenlejeune.tvtime.extensions.lazyPagingItems import com.owenlejeune.tvtime.extensions.lazyPagingItems
import com.owenlejeune.tvtime.extensions.listItems
import com.owenlejeune.tvtime.extensions.shimmerBackground import com.owenlejeune.tvtime.extensions.shimmerBackground
import com.owenlejeune.tvtime.preferences.AppPreferences import com.owenlejeune.tvtime.preferences.AppPreferences
import com.owenlejeune.tvtime.ui.components.ActionsView import com.owenlejeune.tvtime.ui.components.ActionsView
import com.owenlejeune.tvtime.ui.components.AdditionalDetailItem import com.owenlejeune.tvtime.ui.components.AdditionalDetailItem
import com.owenlejeune.tvtime.ui.components.AvatarImage import com.owenlejeune.tvtime.ui.components.AvatarImage
import com.owenlejeune.tvtime.ui.components.BackButton import com.owenlejeune.tvtime.ui.components.BackButton
import com.owenlejeune.tvtime.ui.components.CastCard
import com.owenlejeune.tvtime.ui.components.CastCrewCard import com.owenlejeune.tvtime.ui.components.CastCrewCard
import com.owenlejeune.tvtime.ui.components.ChipDefaults import com.owenlejeune.tvtime.ui.components.ChipDefaults
import com.owenlejeune.tvtime.ui.components.ChipGroup import com.owenlejeune.tvtime.ui.components.ChipGroup
@@ -100,14 +99,11 @@ import com.owenlejeune.tvtime.ui.components.ChipStyle
import com.owenlejeune.tvtime.ui.components.CircleBackgroundColorImage import com.owenlejeune.tvtime.ui.components.CircleBackgroundColorImage
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.ExpandableContentCard
import com.owenlejeune.tvtime.ui.components.ExternalIdsArea import com.owenlejeune.tvtime.ui.components.ExternalIdsArea
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.PlaceholderDetailHeader import com.owenlejeune.tvtime.ui.components.PlaceholderDetailHeader
import com.owenlejeune.tvtime.ui.components.PlaceholderPosterItem
import com.owenlejeune.tvtime.ui.components.PosterItem import com.owenlejeune.tvtime.ui.components.PosterItem
import com.owenlejeune.tvtime.ui.components.RoundedChip import com.owenlejeune.tvtime.ui.components.RoundedChip
import com.owenlejeune.tvtime.ui.components.RoundedTextField import com.owenlejeune.tvtime.ui.components.RoundedTextField
@@ -192,9 +188,6 @@ fun MediaDetailScreen(
if (type == MediaViewType.TV) { if (type == MediaViewType.TV) {
LaunchedEffect(mediaItem) { LaunchedEffect(mediaItem) {
val lastSeason = (mediaItem as DetailedTv?)?.numberOfSeasons ?: 0 val lastSeason = (mediaItem as DetailedTv?)?.numberOfSeasons ?: 0
// for (i in lastSeason downTo 0) {
// mainViewModel.getSeason(itemId, i)
// }
if (lastSeason > 0) { if (lastSeason > 0) {
mainViewModel.getSeason(itemId, lastSeason) mainViewModel.getSeason(itemId, lastSeason)
} }
@@ -351,7 +344,7 @@ fun MediaViewContent(
appNavController = appNavController appNavController = appNavController
) )
CastCard( CastArea(
itemId = itemId, itemId = itemId,
appNavController = appNavController, appNavController = appNavController,
type = type, type = type,
@@ -795,7 +788,7 @@ private fun AdditionalTvItems(
} }
@Composable @Composable
private fun CastCard( private fun CastArea(
itemId: Int, itemId: Int,
type: MediaViewType, type: MediaViewType,
mainViewModel: MainViewModel, mainViewModel: MainViewModel,
@@ -808,65 +801,21 @@ private fun CastCard(
val loadingState = remember { mainViewModel.produceDetailsLoadingStateFor(type) } val loadingState = remember { mainViewModel.produceDetailsLoadingStateFor(type) }
val isLoading = loadingState.value.isIn(LoadingState.LOADING, LoadingState.REFRESHING) val isLoading = loadingState.value.isIn(LoadingState.LOADING, LoadingState.REFRESHING)
ContentCard( CastCard(
modifier = modifier, modifier = modifier,
title = stringResource(R.string.cast_label), title = stringResource(R.string.cast_label),
backgroundColor = MaterialTheme.colorScheme.primary, isLoading = isLoading,
textColor = MaterialTheme.colorScheme.background cast = cast,
) { appNavController = appNavController,
Column { onSeeMore = {
LazyRow( appNavController.navigate(
modifier = Modifier AppNavItem.CastCrewListView.withArgs(
.fillMaxWidth() type,
.padding(vertical = 16.dp), itemId
horizontalArrangement = Arrangement.spacedBy(4.dp)
) {
item {
Spacer(modifier = Modifier.width(8.dp))
}
if (isLoading) {
items(5) {
PlaceholderPosterItem()
}
} else {
items(cast?.size ?: 0) { i ->
cast?.get(i)?.let {
CastCrewCard(appNavController = appNavController, person = it)
}
}
}
item {
Spacer(modifier = Modifier.width(8.dp))
}
}
if (isLoading) {
Text(
text = "",
modifier = Modifier
.padding(start = 12.dp, bottom = 12.dp)
.width(80.dp)
.shimmerBackground(RoundedCornerShape(10.dp))
) )
} else { )
Text(
text = stringResource(R.string.see_all_cast_and_crew),
fontSize = 12.sp,
color = MaterialTheme.colorScheme.inversePrimary,
modifier = Modifier
.padding(start = 12.dp, bottom = 12.dp)
.clickable {
appNavController.navigate(
AppNavItem.CastCrewListView.withArgs(
type,
itemId
)
)
}
)
}
} }
} )
} }
@Composable @Composable

View File

@@ -193,6 +193,7 @@ private fun SeasonContent(
) { ) {
season.episodes.forEach { episode -> season.episodes.forEach { episode ->
DrawEpisodeCard( DrawEpisodeCard(
seriesId = seriesId,
episode = episode, episode = episode,
accountStates = accountStates, accountStates = accountStates,
appNavController = appNavController appNavController = appNavController
@@ -228,23 +229,32 @@ private fun SeasonContent(
@Composable @Composable
private fun DrawEpisodeCard( private fun DrawEpisodeCard(
seriesId: Int,
episode: Episode, episode: Episode,
accountStates: SeasonAccountStates?, accountStates: SeasonAccountStates?,
appNavController: NavController appNavController: NavController
) { ) {
val rating = accountStates?.results?.find { it.id == episode.id }?.takeUnless { !it.isRated }?.rating val rating = accountStates?.results?.find { it.id == episode.id }?.takeUnless { !it.isRated }?.rating
SeasonEpisodeItem(appNavController = appNavController, episode = episode, rating = rating) SeasonEpisodeItem(
appNavController = appNavController,
seriesId = seriesId,
episode = episode,
rating = rating
)
} }
@Composable @Composable
private fun SeasonEpisodeItem( private fun SeasonEpisodeItem(
appNavController: NavController, appNavController: NavController,
seriesId: Int,
episode: Episode, episode: Episode,
rating: Int? rating: Int?
) { ) {
ContentCard { ContentCard {
EpisodeItem( EpisodeItem(
seriesId = seriesId,
episode = episode, episode = episode,
appNavController = appNavController,
elevation = 0.dp, elevation = 0.dp,
maxDescriptionLines = 5, maxDescriptionLines = 5,
rating = rating rating = rating

View File

@@ -174,7 +174,11 @@ private fun SeasonSection(
verticalArrangement = Arrangement.spacedBy(8.dp) verticalArrangement = Arrangement.spacedBy(8.dp)
) { ) {
season.episodes.forEach { episode -> season.episodes.forEach { episode ->
EpisodeItem(episode = episode) EpisodeItem(
seriesId = seriesId,
episode = episode,
appNavController = appNavController
)
} }
} }
} }

View File

@@ -16,6 +16,7 @@ import com.owenlejeune.tvtime.api.tmdb.api.v3.model.AccountStates
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.CastMember import com.owenlejeune.tvtime.api.tmdb.api.v3.model.CastMember
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.CrewMember import com.owenlejeune.tvtime.api.tmdb.api.v3.model.CrewMember
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.DetailedItem import com.owenlejeune.tvtime.api.tmdb.api.v3.model.DetailedItem
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.Episode
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.ImageCollection import com.owenlejeune.tvtime.api.tmdb.api.v3.model.ImageCollection
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.Keyword import com.owenlejeune.tvtime.api.tmdb.api.v3.model.Keyword
@@ -24,6 +25,7 @@ import com.owenlejeune.tvtime.api.tmdb.api.v3.model.SearchResultMedia
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.Video import com.owenlejeune.tvtime.api.tmdb.api.v3.model.Video
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.WatchProviders import com.owenlejeune.tvtime.api.tmdb.api.v3.model.WatchProviders
import com.owenlejeune.tvtime.extensions.createEpisodeKey
import com.owenlejeune.tvtime.ui.screens.tabs.MediaTabNavItem import com.owenlejeune.tvtime.ui.screens.tabs.MediaTabNavItem
import com.owenlejeune.tvtime.utils.types.MediaViewType import com.owenlejeune.tvtime.utils.types.MediaViewType
import com.owenlejeune.tvtime.utils.types.TimeWindow import com.owenlejeune.tvtime.utils.types.TimeWindow
@@ -119,6 +121,12 @@ class MainViewModel: ViewModel(), KoinComponent {
val tvSeasonImages = tvService.seasonImages val tvSeasonImages = tvService.seasonImages
val tvSeasonVideos = tvService.seasonVideos val tvSeasonVideos = tvService.seasonVideos
val tvSeasonWatchProviders = tvService.seasonWatchProviders val tvSeasonWatchProviders = tvService.seasonWatchProviders
val tvEpisodes = tvService.episodesMap
val tvEpisodesAccountStates = tvService.episodeAccountStates
val tvEpisodeCast = tvService.episodeCast
val tvEpisodeCrew = tvService.episodeCrew
val tvEpisodeGuestStars = tvService.episodeGuestStars
val tvEpisodeImages = tvService.episodeImages
val tvDetailsLoadingState = tvService.detailsLoadingState val tvDetailsLoadingState = tvService.detailsLoadingState
val tvImagesLoadingState = tvService.imagesLoadingState val tvImagesLoadingState = tvService.imagesLoadingState
@@ -136,6 +144,10 @@ class MainViewModel: ViewModel(), KoinComponent {
val tvSeasonImagesLoadingState = tvService.seasonImagesLoadingState val tvSeasonImagesLoadingState = tvService.seasonImagesLoadingState
val tvSeasonVideosLoadingState = tvService.seasonVideosLoadingState val tvSeasonVideosLoadingState = tvService.seasonVideosLoadingState
val tvSeasonWatchProvidersLoadingState = tvService.seasonWatchProvidersLoadingState val tvSeasonWatchProvidersLoadingState = tvService.seasonWatchProvidersLoadingState
val tvEpisodeLoadingState = tvService.episodeLoadingState
val tvEpisodeAccountStateLoadingState = tvService.episodeAccountStateLoadingState
val tvEpisodeCreditsLoadingState = tvService.episodeCreditsLoadingState
val tvEpisodeImagesLoadingState = tvService.episodeImagesLoadingState
val isPopularTvLoading = tvService.isPopularTvLoading val isPopularTvLoading = tvService.isPopularTvLoading
val isTopRatedTvLoading = tvService.isTopRatedTvLoading val isTopRatedTvLoading = tvService.isTopRatedTvLoading
@@ -559,6 +571,34 @@ class MainViewModel: ViewModel(), KoinComponent {
} }
} }
suspend fun getEpisode(seriesId: Int, seasonId: Int, episodeId: Int, force: Boolean = false) {
val key = createEpisodeKey(seriesId, seasonId, episodeId)
if (tvEpisodes[key] == null || force) {
tvService.getEpisode(seriesId, seasonId, episodeId, force)
}
}
suspend fun getEpisodeAccountStates(seriesId: Int, seasonId: Int, episodeId: Int, force: Boolean = false) {
val key = createEpisodeKey(seriesId, seasonId, episodeId)
if (tvEpisodesAccountStates[key] == null || force) {
tvService.getEpisodeAccountStates(seriesId, seasonId, episodeId, force)
}
}
suspend fun getEpisodeCredits(seriesId: Int, seasonId: Int, episodeInt: Int, force: Boolean = false) {
val key = createEpisodeKey(seriesId, seasonId, episodeInt)
if (tvEpisodeCast[key] == null || tvEpisodeCrew[key] == null || tvEpisodeGuestStars[key] == null || force) {
tvService.getEpisodeCredits(seriesId, seasonId, episodeInt, force)
}
}
suspend fun getEpisodeImages(seriesId: Int, seasonId: Int, episodeId: Int, force: Boolean = false) {
val key = createEpisodeKey(seriesId, seasonId, episodeId)
if (tvEpisodeImages[key] == null || force) {
tvService.getEpisodeImages(seriesId, seasonId, episodeId, force)
}
}
@SuppressLint("ComposableNaming") @SuppressLint("ComposableNaming")
@Composable @Composable
fun monitorDetailsLoadingRefreshing(refreshing: MutableState<Boolean>) { fun monitorDetailsLoadingRefreshing(refreshing: MutableState<Boolean>) {

View File

@@ -6,5 +6,6 @@ enum class Gender(val rawValue: Int) {
@SerializedName("1") @SerializedName("1")
MALE(1), MALE(1),
@SerializedName("2") @SerializedName("2")
FEMALE(2) FEMALE(2),
UNDEFINED(-1)
} }

View File

@@ -28,6 +28,7 @@
<!-- Headings --> <!-- Headings -->
<string name="cast_label">Cast</string> <string name="cast_label">Cast</string>
<string name="crew_label">Crew</string>
<string name="recommended_label">Recommended</string> <string name="recommended_label">Recommended</string>
<string name="videos_label">Videos</string> <string name="videos_label">Videos</string>
<string name="known_for_label">Known For</string> <string name="known_for_label">Known For</string>