mirror of
https://github.com/owenlejeune/TVTime.git
synced 2025-11-20 10:40:53 -05:00
episode details screen
This commit is contained in:
@@ -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>
|
||||||
}
|
}
|
||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
@@ -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(
|
||||||
|
|||||||
@@ -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
|
||||||
|
)
|
||||||
@@ -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?
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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
|
||||||
)
|
)
|
||||||
@@ -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()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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("_")
|
||||||
@@ -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,6 +150,7 @@ fun DetailHeader(
|
|||||||
horizontalArrangement = Arrangement.spacedBy(20.dp),
|
horizontalArrangement = Arrangement.spacedBy(20.dp),
|
||||||
verticalAlignment = Alignment.Bottom
|
verticalAlignment = Alignment.Bottom
|
||||||
) {
|
) {
|
||||||
|
posterUrl?.let {
|
||||||
PosterItem(
|
PosterItem(
|
||||||
url = posterUrl,
|
url = posterUrl,
|
||||||
title = posterContentDescription,
|
title = posterContentDescription,
|
||||||
@@ -154,6 +158,7 @@ fun DetailHeader(
|
|||||||
overrideShowTitle = false,
|
overrideShowTitle = false,
|
||||||
enabled = 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)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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()
|
||||||
|
|||||||
@@ -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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,54 +801,13 @@ 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(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(vertical = 16.dp),
|
|
||||||
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(
|
appNavController.navigate(
|
||||||
AppNavItem.CastCrewListView.withArgs(
|
AppNavItem.CastCrewListView.withArgs(
|
||||||
type,
|
type,
|
||||||
@@ -864,9 +816,6 @@ private fun CastCard(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>) {
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user