mirror of
https://github.com/owenlejeune/TVTime.git
synced 2025-11-18 09:40:53 -05:00
add more details for movies
This commit is contained in:
@@ -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}"
|
||||
|
||||
@@ -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<CastAndCrew>
|
||||
|
||||
@GET("movie/{id}/release_dates")
|
||||
suspend fun getReleaseDates(@Path("id") id: Int): Response<MovieReleaseResults>
|
||||
|
||||
}
|
||||
@@ -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<PopularMoviesResponse> {
|
||||
return service.getPopularMovies(page)
|
||||
}
|
||||
|
||||
suspend fun getReleaseDates(id: Int): Response<MovieReleaseResults> {
|
||||
return service.getReleaseDates(id)
|
||||
}
|
||||
|
||||
override suspend fun getById(id: Int): Response<out DetailedItem> {
|
||||
return service.getMovieById(id)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -15,6 +15,7 @@ class DetailedTv(
|
||||
@SerializedName("vote_average") override val voteAverage: Float,
|
||||
@SerializedName("created_by") val createdBy: List<Person>,
|
||||
@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<Network>,
|
||||
@SerializedName("number_of_episodes") val numberOfEpisodes: Int,
|
||||
|
||||
@@ -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<ReleaseDateResult>
|
||||
) {
|
||||
|
||||
inner class ReleaseDateResult(
|
||||
@SerializedName("iso_3166_1") val region: String,
|
||||
@SerializedName("release_dates") val releaseDates: List<ReleaseDate>
|
||||
)
|
||||
|
||||
inner class ReleaseDate(
|
||||
@SerializedName("certification") val certification: String,
|
||||
@SerializedName("release_date") val releaseDate: String
|
||||
)
|
||||
|
||||
}
|
||||
@@ -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)
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<String> = emptyList(),
|
||||
onSelectedChanged: (String) -> Unit = {},
|
||||
) {
|
||||
FlowRow(
|
||||
modifier = modifier
|
||||
) {
|
||||
chips.forEach { chip ->
|
||||
Chip(
|
||||
text = chip,
|
||||
onSelectionChanged = onSelectedChanged
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun ChipPreview() {
|
||||
Chip("Test Chip")
|
||||
}
|
||||
@@ -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<DetailedItem?>,
|
||||
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<DetailedItem?>) {
|
||||
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MiscMovieDetails(mediaItem: MutableState<DetailedItem?>, 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<DetailedItem?>) {
|
||||
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<String>) {
|
||||
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
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
}
|
||||
87
app/src/main/java/com/owenlejeune/tvtime/utils/TmdbUtils.kt
Normal file
87
app/src/main/java/com/owenlejeune/tvtime/utils/TmdbUtils.kt
Normal file
@@ -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<String, String>()
|
||||
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 ""
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user