From 4104e6825714c78ed65c6c89d489da847c9ba3eb Mon Sep 17 00:00:00 2001 From: Owen LeJeune Date: Wed, 16 Feb 2022 13:53:26 -0500 Subject: [PATCH] add more details for movies --- app/build.gradle | 1 + .../owenlejeune/tvtime/api/tmdb/MoviesApi.kt | 8 +- .../tvtime/api/tmdb/MoviesService.kt | 12 ++- .../owenlejeune/tvtime/api/tmdb/TmdbUtils.kt | 46 ---------- .../tvtime/api/tmdb/model/DetailedTv.kt | 1 + .../api/tmdb/model/MovieReleaseResults.kt | 19 ++++ .../tvtime/ui/components/Palette.kt | 4 +- .../tvtime/ui/components/Posters.kt | 3 +- .../tvtime/ui/components/Widgets.kt | 59 +++++++++++++ .../tvtime/ui/screens/DetailView.kt | 84 ++++++++++++++++-- .../owenlejeune/tvtime/utils/ComposeUtils.kt | 16 ++++ .../com/owenlejeune/tvtime/utils/TmdbUtils.kt | 87 +++++++++++++++++++ 12 files changed, 276 insertions(+), 64 deletions(-) delete mode 100644 app/src/main/java/com/owenlejeune/tvtime/api/tmdb/TmdbUtils.kt create mode 100644 app/src/main/java/com/owenlejeune/tvtime/api/tmdb/model/MovieReleaseResults.kt create mode 100644 app/src/main/java/com/owenlejeune/tvtime/utils/ComposeUtils.kt create mode 100644 app/src/main/java/com/owenlejeune/tvtime/utils/TmdbUtils.kt diff --git a/app/build.gradle b/app/build.gradle index 8d91d88..8ac010c 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -64,6 +64,7 @@ dependencies { implementation "androidx.activity:activity-compose:${Versions.activity_compose}" implementation "com.google.accompanist:accompanist-systemuicontroller:${Versions.compose_accompanist}" implementation "com.google.accompanist:accompanist-pager:${Versions.compose_accompanist}" + implementation "com.google.accompanist:accompanist-flowlayout:${Versions.compose_accompanist}" implementation "androidx.navigation:navigation-compose:${Versions.compose_navigation}" implementation "androidx.paging:paging-compose:${Versions.compose_paging}" implementation "androidx.constraintlayout:constraintlayout-compose:${Versions.compose_constraint_layout}" diff --git a/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/MoviesApi.kt b/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/MoviesApi.kt index b37c7f3..00f13f7 100644 --- a/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/MoviesApi.kt +++ b/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/MoviesApi.kt @@ -1,9 +1,6 @@ package com.owenlejeune.tvtime.api.tmdb -import com.owenlejeune.tvtime.api.tmdb.model.CastAndCrew -import com.owenlejeune.tvtime.api.tmdb.model.ImageCollection -import com.owenlejeune.tvtime.api.tmdb.model.DetailedMovie -import com.owenlejeune.tvtime.api.tmdb.model.PopularMoviesResponse +import com.owenlejeune.tvtime.api.tmdb.model.* import retrofit2.Response import retrofit2.http.GET import retrofit2.http.Path @@ -23,4 +20,7 @@ interface MoviesApi { @GET("movie/{id}/credits") suspend fun getCastAndCrew(@Path("id") id: Int): Response + @GET("movie/{id}/release_dates") + suspend fun getReleaseDates(@Path("id") id: Int): Response + } \ No newline at end of file diff --git a/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/MoviesService.kt b/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/MoviesService.kt index 09bbbaa..39524ef 100644 --- a/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/MoviesService.kt +++ b/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/MoviesService.kt @@ -1,8 +1,6 @@ package com.owenlejeune.tvtime.api.tmdb -import com.owenlejeune.tvtime.api.tmdb.model.CastAndCrew -import com.owenlejeune.tvtime.api.tmdb.model.ImageCollection -import com.owenlejeune.tvtime.api.tmdb.model.DetailedItem +import com.owenlejeune.tvtime.api.tmdb.model.* import org.koin.core.component.KoinComponent import retrofit2.Response @@ -10,7 +8,13 @@ class MoviesService: KoinComponent, DetailService { private val service by lazy { TmdbClient().createMovieService() } - suspend fun getPopularMovies(page: Int = 1) = service.getPopularMovies(page) + suspend fun getPopularMovies(page: Int = 1): Response { + return service.getPopularMovies(page) + } + + suspend fun getReleaseDates(id: Int): Response { + return service.getReleaseDates(id) + } override suspend fun getById(id: Int): Response { return service.getMovieById(id) diff --git a/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/TmdbUtils.kt b/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/TmdbUtils.kt deleted file mode 100644 index 45f75cf..0000000 --- a/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/TmdbUtils.kt +++ /dev/null @@ -1,46 +0,0 @@ -package com.owenlejeune.tvtime.api.tmdb - -import com.owenlejeune.tvtime.api.tmdb.model.DetailedItem -import com.owenlejeune.tvtime.api.tmdb.model.Image -import com.owenlejeune.tvtime.api.tmdb.model.Person -import com.owenlejeune.tvtime.api.tmdb.model.TmdbItem - -object TmdbUtils { - - private const val POSTER_BASE = "https://image.tmdb.org/t/p/original" - private const val BACKDROP_BASE = "https://www.themoviedb.org/t/p/original" - private const val PERSON_BASE = "https://www.themoviedb.org/t/p/w600_and_h900_bestv2" - - fun getFullPosterPath(posterPath: String?): String? { - return posterPath?.let { "https://image.tmdb.org/t/p/original${posterPath}" } - } - - fun getFullPosterPath(tmdbItem: TmdbItem?): String? { - return tmdbItem?.let { getFullPosterPath(tmdbItem.posterPath) } - } - - fun getFullPosterPath(image: Image): String? { - return getFullPosterPath(image.filePath) - } - - fun getFullBackdropPath(backdropPath: String?): String? { - return backdropPath?.let { "https://www.themoviedb.org/t/p/original${backdropPath}" } - } - - fun getFullBackdropPath(detailItem: DetailedItem?): String? { - return detailItem?.let { getFullBackdropPath(detailItem.backdropPath) } - } - - fun getFullBackdropPath(image: Image): String? { - return getFullBackdropPath(image.filePath) - } - - fun getFullPersonImagePath(path: String?): String? { - return path?.let { "https://www.themoviedb.org/t/p/w600_and_h900_bestv2${path}" } - } - - fun getFullPersonImagePath(person: Person): String? { - return getFullPersonImagePath(person.profilePath) - } - -} \ No newline at end of file diff --git a/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/model/DetailedTv.kt b/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/model/DetailedTv.kt index ec4c44a..e92293b 100644 --- a/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/model/DetailedTv.kt +++ b/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/model/DetailedTv.kt @@ -15,6 +15,7 @@ class DetailedTv( @SerializedName("vote_average") override val voteAverage: Float, @SerializedName("created_by") val createdBy: List, @SerializedName("first_air_date") val firstAirDate: String, + @SerializedName("last_air_date") val lastAirDate: String, @SerializedName("in_production") val inProduction: Boolean, @SerializedName("networks") val networks: List, @SerializedName("number_of_episodes") val numberOfEpisodes: Int, diff --git a/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/model/MovieReleaseResults.kt b/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/model/MovieReleaseResults.kt new file mode 100644 index 0000000..0195726 --- /dev/null +++ b/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/model/MovieReleaseResults.kt @@ -0,0 +1,19 @@ +package com.owenlejeune.tvtime.api.tmdb.model + +import com.google.gson.annotations.SerializedName + +data class MovieReleaseResults( + @SerializedName("results") val releaseDates: List +) { + + inner class ReleaseDateResult( + @SerializedName("iso_3166_1") val region: String, + @SerializedName("release_dates") val releaseDates: List + ) + + inner class ReleaseDate( + @SerializedName("certification") val certification: String, + @SerializedName("release_date") val releaseDate: String + ) + +} diff --git a/app/src/main/java/com/owenlejeune/tvtime/ui/components/Palette.kt b/app/src/main/java/com/owenlejeune/tvtime/ui/components/Palette.kt index 2ab7d5c..8e18fdd 100644 --- a/app/src/main/java/com/owenlejeune/tvtime/ui/components/Palette.kt +++ b/app/src/main/java/com/owenlejeune/tvtime/ui/components/Palette.kt @@ -14,6 +14,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp import com.owenlejeune.tvtime.extensions.listItems +import com.owenlejeune.tvtime.utils.ComposeUtils @Composable fun PaletteView() { @@ -57,8 +58,9 @@ private fun PaletteItemView(surfaceColor: Color, textColor: Color, text: String) shape = RoundedCornerShape(30.dp), backgroundColor = surfaceColor ) { + val hexString = ComposeUtils.colorToHexString(surfaceColor) Text( - text = text, + text = "${text}\n${hexString}", color = textColor, modifier = Modifier.padding(30.dp, 12.dp) ) diff --git a/app/src/main/java/com/owenlejeune/tvtime/ui/components/Posters.kt b/app/src/main/java/com/owenlejeune/tvtime/ui/components/Posters.kt index 4fb8533..bc62e3e 100644 --- a/app/src/main/java/com/owenlejeune/tvtime/ui/components/Posters.kt +++ b/app/src/main/java/com/owenlejeune/tvtime/ui/components/Posters.kt @@ -30,12 +30,11 @@ import com.google.accompanist.pager.ExperimentalPagerApi import com.google.accompanist.pager.HorizontalPager import com.google.accompanist.pager.rememberPagerState import com.owenlejeune.tvtime.R -import com.owenlejeune.tvtime.api.tmdb.TmdbUtils +import com.owenlejeune.tvtime.utils.TmdbUtils import com.owenlejeune.tvtime.api.tmdb.model.ImageCollection import com.owenlejeune.tvtime.api.tmdb.model.TmdbItem import com.owenlejeune.tvtime.extensions.dpToPx import com.owenlejeune.tvtime.extensions.listItems -import kotlinx.coroutines.* @OptIn(ExperimentalFoundationApi::class) @Composable diff --git a/app/src/main/java/com/owenlejeune/tvtime/ui/components/Widgets.kt b/app/src/main/java/com/owenlejeune/tvtime/ui/components/Widgets.kt index 8972abc..66badc1 100644 --- a/app/src/main/java/com/owenlejeune/tvtime/ui/components/Widgets.kt +++ b/app/src/main/java/com/owenlejeune/tvtime/ui/components/Widgets.kt @@ -6,6 +6,8 @@ import androidx.compose.animation.core.animateFloatAsState import androidx.compose.foundation.* import androidx.compose.foundation.gestures.detectTapGestures import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.selection.toggleable import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.Card import androidx.compose.material.icons.Icons @@ -36,6 +38,7 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.TextUnit import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import com.google.accompanist.flowlayout.FlowRow @Composable fun TopLevelSwitch( @@ -206,4 +209,60 @@ fun MinLinesText( onTextLayout = onTextLayout, style = style ) +} + +@Composable +fun Chip( + text: String, + style: TextStyle = MaterialTheme.typography.bodySmall, + isSelected: Boolean = true, + onSelectionChanged: (String) -> Unit = {} +) { + Surface( + modifier = Modifier.padding(4.dp), + shadowElevation = 8.dp, + shape = RoundedCornerShape(5.dp), + color = if (isSelected) MaterialTheme.colorScheme.secondaryContainer else MaterialTheme.colorScheme.secondary + ) { + Row( + modifier = Modifier + .toggleable( + value = isSelected, + onValueChange = { + onSelectionChanged(text) + } + ) + ) { + Text( + text = text, + style = style, + color = if (isSelected) MaterialTheme.colorScheme.onSecondaryContainer else MaterialTheme.colorScheme.onSecondary, + modifier = Modifier.padding(8.dp) + ) + } + } +} + +@Composable +fun ChipGroup( + modifier: Modifier = Modifier, + chips: List = emptyList(), + onSelectedChanged: (String) -> Unit = {}, +) { + FlowRow( + modifier = modifier + ) { + chips.forEach { chip -> + Chip( + text = chip, + onSelectionChanged = onSelectedChanged + ) + } + } +} + +@Preview +@Composable +fun ChipPreview() { + Chip("Test Chip") } \ No newline at end of file diff --git a/app/src/main/java/com/owenlejeune/tvtime/ui/screens/DetailView.kt b/app/src/main/java/com/owenlejeune/tvtime/ui/screens/DetailView.kt index fb1ab33..8559109 100644 --- a/app/src/main/java/com/owenlejeune/tvtime/ui/screens/DetailView.kt +++ b/app/src/main/java/com/owenlejeune/tvtime/ui/screens/DetailView.kt @@ -28,11 +28,12 @@ import coil.transform.RoundedCornersTransformation import com.owenlejeune.tvtime.R import com.owenlejeune.tvtime.api.tmdb.DetailService import com.owenlejeune.tvtime.api.tmdb.MoviesService -import com.owenlejeune.tvtime.api.tmdb.TmdbUtils +import com.owenlejeune.tvtime.utils.TmdbUtils import com.owenlejeune.tvtime.api.tmdb.TvService import com.owenlejeune.tvtime.api.tmdb.model.* import com.owenlejeune.tvtime.extensions.dpToPx import com.owenlejeune.tvtime.ui.components.BackdropImage +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 @@ -46,7 +47,6 @@ fun DetailView( itemId: Int?, type: DetailViewType ) { - val context = LocalContext.current val service = when(type) { DetailViewType.MOVIE -> MoviesService() DetailViewType.TV -> TvService() @@ -109,11 +109,12 @@ fun DetailView( ContentColumn( modifier = Modifier.constrainAs(contentColumn) { - top.linkTo(backdropImage.bottom, margin = 8.dp) + top.linkTo(backdropImage.bottom)//, margin = 8.dp) }, itemId = itemId, mediaItem = mediaItem, - service = service + service = service, + mediaType = type ) } } @@ -176,27 +177,84 @@ private fun BackButton(modifier: Modifier, appNavController: NavController) { private fun ContentColumn(modifier: Modifier, itemId: Int?, mediaItem: MutableState, - service: DetailService + service: DetailService, + mediaType: DetailViewType ) { Column( modifier = modifier .fillMaxWidth() .wrapContentHeight() - .padding(horizontal = 16.dp) + .padding(start = 16.dp, end = 16.dp, bottom = 16.dp) ) { + if (mediaType == DetailViewType.MOVIE) { + MiscMovieDetails(mediaItem = mediaItem, service as MoviesService) + } else { + MiscTvDetails(mediaItem = mediaItem) + } + OverviewCard(mediaItem = mediaItem) CastCard(itemId = itemId, service = service) } } +@Composable +private fun MiscTvDetails(mediaItem: MutableState) { + +} + +@Composable +private fun MiscMovieDetails(mediaItem: MutableState, service: MoviesService) { + mediaItem.value?.let { mi -> + val movie = mi as DetailedMovie + + val contentRating = remember { mutableStateOf("") } + fetchMovieContentRating(movie.id, service, contentRating) + + Column( + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight() + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight() + .padding(start = 8.dp, end = 10.dp, bottom = 8.dp) + ) { + val releaseYear = TmdbUtils.getMovieReleaseYear(movie) + Text(text = releaseYear, color = MaterialTheme.colorScheme.onBackground) + val runtime = TmdbUtils.convertRuntimeToHoursMinutes(movie) + Text( + 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 } + ) + } + } +} + @Composable private fun OverviewCard(mediaItem: MutableState) { Card( modifier = Modifier .fillMaxWidth() .wrapContentHeight() - .padding(bottom = 12.dp), + .padding(bottom = 16.dp), shape = RoundedCornerShape(10.dp), backgroundColor = MaterialTheme.colorScheme.surfaceVariant, elevation = 8.dp @@ -320,6 +378,18 @@ private fun fetchCastAndCrew(id: Int, service: DetailService, castAndCrew: Mutab } } +private fun fetchMovieContentRating(id: Int, service: MoviesService, contentRating: MutableState) { + CoroutineScope(Dispatchers.IO).launch { + val results = service.getReleaseDates(id) + if (results.isSuccessful) { + val cr = TmdbUtils.getMovieRating(results.body()) + withContext(Dispatchers.Main) { + contentRating.value = cr + } + } + } +} + enum class DetailViewType { MOVIE, TV diff --git a/app/src/main/java/com/owenlejeune/tvtime/utils/ComposeUtils.kt b/app/src/main/java/com/owenlejeune/tvtime/utils/ComposeUtils.kt new file mode 100644 index 0000000..a651249 --- /dev/null +++ b/app/src/main/java/com/owenlejeune/tvtime/utils/ComposeUtils.kt @@ -0,0 +1,16 @@ +package com.owenlejeune.tvtime.utils + +import androidx.compose.ui.graphics.Color + +object ComposeUtils { + + fun colorToHexString(color: Color): String { + val a = (color.alpha * 255f).toInt().toString(16).uppercase() + val r = (color.red * 255f).toInt().toString(16).uppercase() + val g = (color.green * 255f).toInt().toString(16).uppercase() + val b = (color.blue * 255f).toInt().toString(16).uppercase() + + return "0x$a$r$g$b" + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/owenlejeune/tvtime/utils/TmdbUtils.kt b/app/src/main/java/com/owenlejeune/tvtime/utils/TmdbUtils.kt new file mode 100644 index 0000000..49b66a3 --- /dev/null +++ b/app/src/main/java/com/owenlejeune/tvtime/utils/TmdbUtils.kt @@ -0,0 +1,87 @@ +package com.owenlejeune.tvtime.utils + +import androidx.compose.ui.text.intl.Locale +import com.owenlejeune.tvtime.api.tmdb.model.* + +object TmdbUtils { + + private const val POSTER_BASE = "https://image.tmdb.org/t/p/original" + private const val BACKDROP_BASE = "https://www.themoviedb.org/t/p/original" + private const val PERSON_BASE = "https://www.themoviedb.org/t/p/w600_and_h900_bestv2" + + fun getFullPosterPath(posterPath: String?): String? { + return posterPath?.let { "https://image.tmdb.org/t/p/original${posterPath}" } + } + + fun getFullPosterPath(tmdbItem: TmdbItem?): String? { + return tmdbItem?.let { getFullPosterPath(tmdbItem.posterPath) } + } + + fun getFullPosterPath(image: Image): String? { + return getFullPosterPath(image.filePath) + } + + fun getFullBackdropPath(backdropPath: String?): String? { + return backdropPath?.let { "https://www.themoviedb.org/t/p/original${backdropPath}" } + } + + fun getFullBackdropPath(detailItem: DetailedItem?): String? { + return detailItem?.let { getFullBackdropPath(detailItem.backdropPath) } + } + + fun getFullBackdropPath(image: Image): String? { + return getFullBackdropPath(image.filePath) + } + + fun getFullPersonImagePath(path: String?): String? { + return path?.let { "https://www.themoviedb.org/t/p/w600_and_h900_bestv2${path}" } + } + + fun getFullPersonImagePath(person: Person): String? { + return getFullPersonImagePath(person.profilePath) + } + + fun getMovieReleaseYear(movie: DetailedMovie): String { + return movie.releaseDate.split("-")[0] + } + + fun getTvStartYear(series: DetailedTv): String { + return series.firstAirDate.split("-")[0] + } + + fun getTvEndYear(series: DetailedTv): String { + return series.lastAirDate.split("-")[0] + } + + fun convertRuntimeToHoursMinutes(movie: DetailedMovie): String { + movie.runtime?.let { runtime -> + val hours = runtime / 60 + val minutes = runtime % 60 + return "${hours}h${minutes}" + } + return "" + } + + fun getMovieRating(releases: MovieReleaseResults?): String { + if (releases == null) { + return "" + } + + val defRegion = "US" + val currentRegion = Locale.current.language + val certifications = HashMap() + releases.releaseDates.forEach { releaseDateResult -> + if (releaseDateResult.region == currentRegion || releaseDateResult.region == defRegion) { + val cert = releaseDateResult.releaseDates.firstOrNull { it.certification.isNotEmpty() } + if (cert != null) { + certifications[releaseDateResult.region] = cert.certification + } + } + } + if (certifications.isNotEmpty()) { + return certifications[currentRegion] ?: certifications[defRegion] ?: "" + } + return "" + } + +} \ No newline at end of file