mirror of
https://github.com/owenlejeune/TVTime.git
synced 2025-12-27 10:01:20 -05:00
display more detailed information
This commit is contained in:
@@ -1,9 +1,6 @@
|
|||||||
package com.owenlejeune.tvtime.api.tmdb
|
package com.owenlejeune.tvtime.api.tmdb
|
||||||
|
|
||||||
import com.owenlejeune.tvtime.api.tmdb.model.CastAndCrew
|
import com.owenlejeune.tvtime.api.tmdb.model.*
|
||||||
import com.owenlejeune.tvtime.api.tmdb.model.ImageCollection
|
|
||||||
import com.owenlejeune.tvtime.api.tmdb.model.PopularTvResponse
|
|
||||||
import com.owenlejeune.tvtime.api.tmdb.model.DetailedTv
|
|
||||||
import retrofit2.Response
|
import retrofit2.Response
|
||||||
import retrofit2.http.GET
|
import retrofit2.http.GET
|
||||||
import retrofit2.http.Path
|
import retrofit2.http.Path
|
||||||
@@ -23,4 +20,7 @@ interface TvApi {
|
|||||||
@GET("tv/{id}/credits")
|
@GET("tv/{id}/credits")
|
||||||
suspend fun getCastAndCrew(@Path("id") id: Int): Response<CastAndCrew>
|
suspend fun getCastAndCrew(@Path("id") id: Int): Response<CastAndCrew>
|
||||||
|
|
||||||
|
@GET("tv/{id}/content_ratings")
|
||||||
|
suspend fun getContentRatings(@Path("id") id: Int): Response<TvContentRatings>
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -3,6 +3,7 @@ package com.owenlejeune.tvtime.api.tmdb
|
|||||||
import com.owenlejeune.tvtime.api.tmdb.model.CastAndCrew
|
import com.owenlejeune.tvtime.api.tmdb.model.CastAndCrew
|
||||||
import com.owenlejeune.tvtime.api.tmdb.model.ImageCollection
|
import com.owenlejeune.tvtime.api.tmdb.model.ImageCollection
|
||||||
import com.owenlejeune.tvtime.api.tmdb.model.DetailedItem
|
import com.owenlejeune.tvtime.api.tmdb.model.DetailedItem
|
||||||
|
import com.owenlejeune.tvtime.api.tmdb.model.TvContentRatings
|
||||||
import org.koin.core.component.KoinComponent
|
import org.koin.core.component.KoinComponent
|
||||||
import retrofit2.Response
|
import retrofit2.Response
|
||||||
|
|
||||||
@@ -23,4 +24,8 @@ class TvService: KoinComponent, DetailService {
|
|||||||
override suspend fun getCastAndCrew(id: Int): Response<CastAndCrew> {
|
override suspend fun getCastAndCrew(id: Int): Response<CastAndCrew> {
|
||||||
return service.getCastAndCrew(id)
|
return service.getCastAndCrew(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun getContentRatings(id: Int): Response<TvContentRatings> {
|
||||||
|
return service.getContentRatings(id)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -20,5 +20,6 @@ class DetailedTv(
|
|||||||
@SerializedName("networks") val networks: List<Network>,
|
@SerializedName("networks") val networks: List<Network>,
|
||||||
@SerializedName("number_of_episodes") val numberOfEpisodes: Int,
|
@SerializedName("number_of_episodes") val numberOfEpisodes: Int,
|
||||||
@SerializedName("number_of_seasons") val numberOfSeasons: Int,
|
@SerializedName("number_of_seasons") val numberOfSeasons: Int,
|
||||||
@SerializedName("seasons") val seasons: List<Season>
|
@SerializedName("seasons") val seasons: List<Season>,
|
||||||
|
@SerializedName("episode_run_time") val episodeRuntime: List<Int>
|
||||||
): DetailedItem(id, title, posterPath, backdropPath, genres, overview, productionCompanies, status, tagline, voteAverage)
|
): DetailedItem(id, title, posterPath, backdropPath, genres, overview, productionCompanies, status, tagline, voteAverage)
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package com.owenlejeune.tvtime.api.tmdb.model
|
||||||
|
|
||||||
|
import com.google.gson.annotations.SerializedName
|
||||||
|
|
||||||
|
class TvContentRatings(
|
||||||
|
@SerializedName("results") val results: List<TvContentRating>
|
||||||
|
) {
|
||||||
|
|
||||||
|
inner class TvContentRating(
|
||||||
|
@SerializedName("iso_3166_1") val language: String,
|
||||||
|
@SerializedName("rating") val rating: String
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
||||||
@@ -265,4 +265,37 @@ fun ChipGroup(
|
|||||||
@Composable
|
@Composable
|
||||||
fun ChipPreview() {
|
fun ChipPreview() {
|
||||||
Chip("Test Chip")
|
Chip("Test Chip")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun RatingRing(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
progress: Float = 0f,
|
||||||
|
textColor: Color = Color.White
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = modifier
|
||||||
|
.size(60.dp)
|
||||||
|
.padding(8.dp)
|
||||||
|
) {
|
||||||
|
CircularProgressIndicator(
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
progress = progress,
|
||||||
|
strokeWidth = 4.dp,
|
||||||
|
color = MaterialTheme.colorScheme.primary
|
||||||
|
)
|
||||||
|
|
||||||
|
Text(
|
||||||
|
modifier = Modifier.align(Alignment.Center),
|
||||||
|
text = "${(progress*100).toInt()}%",
|
||||||
|
color = textColor,
|
||||||
|
style = MaterialTheme.typography.titleSmall
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
fun RatingRingPreview() {
|
||||||
|
RatingRing(progress = 0.5f)
|
||||||
}
|
}
|
||||||
@@ -32,10 +32,7 @@ import com.owenlejeune.tvtime.utils.TmdbUtils
|
|||||||
import com.owenlejeune.tvtime.api.tmdb.TvService
|
import com.owenlejeune.tvtime.api.tmdb.TvService
|
||||||
import com.owenlejeune.tvtime.api.tmdb.model.*
|
import com.owenlejeune.tvtime.api.tmdb.model.*
|
||||||
import com.owenlejeune.tvtime.extensions.dpToPx
|
import com.owenlejeune.tvtime.extensions.dpToPx
|
||||||
import com.owenlejeune.tvtime.ui.components.BackdropImage
|
import com.owenlejeune.tvtime.ui.components.*
|
||||||
import com.owenlejeune.tvtime.ui.components.ChipGroup
|
|
||||||
import com.owenlejeune.tvtime.ui.components.MinLinesText
|
|
||||||
import com.owenlejeune.tvtime.ui.components.PosterItem
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
@@ -186,21 +183,39 @@ private fun ContentColumn(modifier: Modifier,
|
|||||||
.wrapContentHeight()
|
.wrapContentHeight()
|
||||||
.padding(start = 16.dp, end = 16.dp, bottom = 16.dp)
|
.padding(start = 16.dp, end = 16.dp, bottom = 16.dp)
|
||||||
) {
|
) {
|
||||||
|
|
||||||
if (mediaType == DetailViewType.MOVIE) {
|
if (mediaType == DetailViewType.MOVIE) {
|
||||||
MiscMovieDetails(mediaItem = mediaItem, service as MoviesService)
|
MiscMovieDetails(mediaItem = mediaItem, service as MoviesService)
|
||||||
} else {
|
} else {
|
||||||
MiscTvDetails(mediaItem = mediaItem)
|
MiscTvDetails(mediaItem = mediaItem, service as TvService)
|
||||||
}
|
}
|
||||||
|
|
||||||
OverviewCard(mediaItem = mediaItem)
|
if (mediaItem.value?.overview?.isNotEmpty() == true) {
|
||||||
|
OverviewCard(mediaItem = mediaItem)
|
||||||
|
}
|
||||||
|
|
||||||
CastCard(itemId = itemId, service = service)
|
CastCard(itemId = itemId, service = service)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun MiscTvDetails(mediaItem: MutableState<DetailedItem?>) {
|
private fun MiscTvDetails(mediaItem: MutableState<DetailedItem?>, service: TvService) {
|
||||||
|
mediaItem.value?.let { tv ->
|
||||||
|
val series = tv as DetailedTv
|
||||||
|
|
||||||
|
val contentRating = remember { mutableStateOf("") }
|
||||||
|
fetchTvContentRating(series.id, service, contentRating)
|
||||||
|
|
||||||
|
MiscDetails(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.wrapContentHeight(),
|
||||||
|
year = TmdbUtils.getSeriesRun(series),
|
||||||
|
runtime = TmdbUtils.convertRuntimeToHoursMinutes(series),
|
||||||
|
genres = series.genres,
|
||||||
|
contentRating = contentRating
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -211,40 +226,55 @@ private fun MiscMovieDetails(mediaItem: MutableState<DetailedItem?>, service: Mo
|
|||||||
val contentRating = remember { mutableStateOf("") }
|
val contentRating = remember { mutableStateOf("") }
|
||||||
fetchMovieContentRating(movie.id, service, contentRating)
|
fetchMovieContentRating(movie.id, service, contentRating)
|
||||||
|
|
||||||
Column(
|
MiscDetails(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.wrapContentHeight(),
|
||||||
|
year = TmdbUtils.getMovieReleaseYear(movie),
|
||||||
|
runtime = TmdbUtils.convertRuntimeToHoursMinutes(movie),
|
||||||
|
genres = movie.genres,
|
||||||
|
contentRating = contentRating
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun MiscDetails(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
year: String,
|
||||||
|
runtime: String,
|
||||||
|
genres: List<Genre>,
|
||||||
|
contentRating: MutableState<String>
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = modifier
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.wrapContentHeight()
|
.wrapContentHeight()
|
||||||
|
.padding(start = 8.dp, end = 10.dp, bottom = 8.dp)
|
||||||
) {
|
) {
|
||||||
Row(
|
Text(text = year, color = MaterialTheme.colorScheme.onBackground)
|
||||||
modifier = Modifier
|
Text(
|
||||||
.fillMaxWidth()
|
text = runtime,
|
||||||
.wrapContentHeight()
|
color = MaterialTheme.colorScheme.onBackground,
|
||||||
.padding(start = 8.dp, end = 10.dp, bottom = 8.dp)
|
modifier = Modifier.padding(start = 12.dp)
|
||||||
) {
|
)
|
||||||
val releaseYear = TmdbUtils.getMovieReleaseYear(movie)
|
Text(
|
||||||
Text(text = releaseYear, color = MaterialTheme.colorScheme.onBackground)
|
text = contentRating.value,
|
||||||
val runtime = TmdbUtils.convertRuntimeToHoursMinutes(movie)
|
color = MaterialTheme.colorScheme.onBackground,
|
||||||
Text(
|
modifier = Modifier.padding(start = 12.dp)
|
||||||
text = runtime,
|
|
||||||
color = MaterialTheme.colorScheme.onBackground,
|
|
||||||
modifier = Modifier.padding(start = 12.dp)
|
|
||||||
)
|
|
||||||
Text(
|
|
||||||
text = contentRating.value,
|
|
||||||
color = MaterialTheme.colorScheme.onBackground,
|
|
||||||
modifier = Modifier.padding(start = 12.dp)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
ChipGroup(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.wrapContentHeight()
|
|
||||||
.padding(bottom = 8.dp),
|
|
||||||
chips = movie.genres.map { it.name }
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ChipGroup(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.wrapContentHeight()
|
||||||
|
.padding(bottom = 8.dp),
|
||||||
|
chips = genres.map { it.name }
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -314,7 +344,7 @@ private fun CastCrewCard(person: Person) {
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.size(width = 120.dp, height = 180.dp),
|
.size(width = 120.dp, height = 180.dp),
|
||||||
painter = rememberImagePainter(
|
painter = rememberImagePainter(
|
||||||
data = TmdbUtils.getFullPersonImagePath(person),
|
data = TmdbUtils.getFullPersonImagePath(person) ?: R.drawable.no_person_photo,
|
||||||
builder = {
|
builder = {
|
||||||
transformations(RoundedCornersTransformation(5f.dpToPx(context)))
|
transformations(RoundedCornersTransformation(5f.dpToPx(context)))
|
||||||
placeholder(R.drawable.placeholder)
|
placeholder(R.drawable.placeholder)
|
||||||
@@ -390,6 +420,18 @@ private fun fetchMovieContentRating(id: Int, service: MoviesService, contentRati
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun fetchTvContentRating(id: Int, service: TvService, contentRating: MutableState<String>) {
|
||||||
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
|
val results = service.getContentRatings(id)
|
||||||
|
if (results.isSuccessful) {
|
||||||
|
val cr = TmdbUtils.getTvRating(results.body())
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
contentRating.value = cr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
enum class DetailViewType {
|
enum class DetailViewType {
|
||||||
MOVIE,
|
MOVIE,
|
||||||
TV
|
TV
|
||||||
|
|||||||
@@ -45,6 +45,16 @@ object TmdbUtils {
|
|||||||
return movie.releaseDate.split("-")[0]
|
return movie.releaseDate.split("-")[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getSeriesRun(series: DetailedTv): String {
|
||||||
|
val startYear = getTvStartYear(series)
|
||||||
|
val endYear = if (series.status == "Active") {
|
||||||
|
getTvEndYear(series)
|
||||||
|
} else {
|
||||||
|
""
|
||||||
|
}
|
||||||
|
return "${startYear}-${endYear}"
|
||||||
|
}
|
||||||
|
|
||||||
fun getTvStartYear(series: DetailedTv): String {
|
fun getTvStartYear(series: DetailedTv): String {
|
||||||
return series.firstAirDate.split("-")[0]
|
return series.firstAirDate.split("-")[0]
|
||||||
}
|
}
|
||||||
@@ -55,13 +65,29 @@ object TmdbUtils {
|
|||||||
|
|
||||||
fun convertRuntimeToHoursMinutes(movie: DetailedMovie): String {
|
fun convertRuntimeToHoursMinutes(movie: DetailedMovie): String {
|
||||||
movie.runtime?.let { runtime ->
|
movie.runtime?.let { runtime ->
|
||||||
val hours = runtime / 60
|
return convertRuntimeToHoursAndMinutes(runtime)
|
||||||
val minutes = runtime % 60
|
|
||||||
return "${hours}h${minutes}"
|
|
||||||
}
|
}
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun convertRuntimeToHoursMinutes(series: DetailedTv): String {
|
||||||
|
return convertRuntimeToHoursAndMinutes(series.episodeRuntime[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun convertRuntimeToHoursAndMinutes(runtime: Int): String {
|
||||||
|
val hours = runtime / 60
|
||||||
|
val minutes = runtime % 60
|
||||||
|
return if (hours > 0){
|
||||||
|
if (minutes > 0) {
|
||||||
|
"${hours}h${minutes}"
|
||||||
|
} else {
|
||||||
|
"${hours}h"
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
"${minutes}m"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun getMovieRating(releases: MovieReleaseResults?): String {
|
fun getMovieRating(releases: MovieReleaseResults?): String {
|
||||||
if (releases == null) {
|
if (releases == null) {
|
||||||
return ""
|
return ""
|
||||||
@@ -84,4 +110,27 @@ object TmdbUtils {
|
|||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getTvRating(contentRatings: TvContentRatings?): String {
|
||||||
|
if (contentRatings == null) {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
val defRegion = "US"
|
||||||
|
val currentRegion = Locale.current.language
|
||||||
|
val certifications = HashMap<String, String>()
|
||||||
|
contentRatings.results.forEach { contentRating ->
|
||||||
|
if (contentRating.language == currentRegion || contentRating.language == defRegion) {
|
||||||
|
certifications[contentRating.language] = contentRating.rating
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (certifications.isNotEmpty()) {
|
||||||
|
return certifications[currentRegion] ?: certifications[defRegion] ?: ""
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
fun convertVoteAverageToPercentage(detailItem: DetailedItem): Float {
|
||||||
|
return detailItem.voteAverage / 10f
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
10
app/src/main/res/drawable/ic_person.xml
Normal file
10
app/src/main/res/drawable/ic_person.xml
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24"
|
||||||
|
android:tint="?attr/colorControlNormal">
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/white"
|
||||||
|
android:pathData="M12,12c2.21,0 4,-1.79 4,-4s-1.79,-4 -4,-4 -4,1.79 -4,4 1.79,4 4,4zM12,14c-2.67,0 -8,1.34 -8,4v2h16v-2c0,-2.66 -5.33,-4 -8,-4z"/>
|
||||||
|
</vector>
|
||||||
5
app/src/main/res/drawable/no_person_photo.xml
Normal file
5
app/src/main/res/drawable/no_person_photo.xml
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<item android:drawable="@drawable/placeholder"/>
|
||||||
|
<item android:drawable="@drawable/ic_person" android:gravity="center"/>
|
||||||
|
</layer-list>
|
||||||
Reference in New Issue
Block a user