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 index 301e1a4..c4170f2 100644 --- a/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/TmdbUtils.kt +++ b/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/TmdbUtils.kt @@ -1,15 +1,24 @@ package com.owenlejeune.tvtime.api.tmdb +import com.owenlejeune.tvtime.api.tmdb.model.DetailedItem import com.owenlejeune.tvtime.api.tmdb.model.TmdbItem object TmdbUtils { - fun getFullPosterPath(posterPath: String?): String { - return "https://image.tmdb.org/t/p/original${posterPath}" + fun getFullPosterPath(posterPath: String?): String? { + return posterPath?.let { "https://image.tmdb.org/t/p/original${posterPath}" } } - fun getFullPosterPath(tmdbItem: TmdbItem): String { - return getFullPosterPath(tmdbItem.posterPath) + fun getFullPosterPath(tmdbItem: TmdbItem?): String? { + return tmdbItem?.let { getFullPosterPath(tmdbItem.posterPath) } + } + + 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) } } } \ No newline at end of file diff --git a/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/model/DetailedItem.kt b/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/model/DetailedItem.kt index 568531f..29c1fca 100644 --- a/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/model/DetailedItem.kt +++ b/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/model/DetailedItem.kt @@ -1,3 +1,13 @@ package com.owenlejeune.tvtime.api.tmdb.model -abstract class DetailedItem(id: Int, title: String, posterPath: String?): TmdbItem(id, title, posterPath) \ No newline at end of file +abstract class DetailedItem( + id: Int, + title: String, + posterPath: String?, + @Transient open val backdropPath: String?, + @Transient open val genres: List, + @Transient open val overview: String?, + @Transient open val productionCompanies: List, + @Transient open val status: String, + @Transient open val tagline: String? +): TmdbItem(id, title, posterPath) \ No newline at end of file diff --git a/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/model/DetailedMovie.kt b/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/model/DetailedMovie.kt index 9818150..efb3221 100644 --- a/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/model/DetailedMovie.kt +++ b/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/model/DetailedMovie.kt @@ -5,5 +5,16 @@ import com.google.gson.annotations.SerializedName class DetailedMovie( @SerializedName("id") override val id: Int, @SerializedName("original_title") override val title: String, - @SerializedName("poster_path") override val posterPath: String? -): DetailedItem(id, title, posterPath) + @SerializedName("poster_path") override val posterPath: String?, + @SerializedName("backdrop_path") override val backdropPath: String?, + @SerializedName("genres") override val genres: List, + @SerializedName("overview") override val overview: String?, + @SerializedName("production_companies") override val productionCompanies: List, + @SerializedName("status") override val status: String, + @SerializedName("tagline") override val tagline: String?, + @SerializedName("adult") val isAdult: Boolean, + @SerializedName("budget") val budget: Int, + @SerializedName("release_date") val releaseDate: String, + @SerializedName("revenue") val revenue: Int, + @SerializedName("runtime") val runtime: Int? +): DetailedItem(id, title, posterPath, backdropPath, genres, overview, productionCompanies, status, tagline) 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 0c7990f..34636c5 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 @@ -5,5 +5,18 @@ import com.google.gson.annotations.SerializedName class DetailedTv( @SerializedName("id") override val id: Int, @SerializedName("name") override val title: String, - @SerializedName("poster_path") override val posterPath: String? -): DetailedItem(id, title, posterPath) \ No newline at end of file + @SerializedName("poster_path") override val posterPath: String?, + @SerializedName("backdrop_path") override val backdropPath: String?, + @SerializedName("genres") override val genres: List, + @SerializedName("overview") override val overview: String?, + @SerializedName("production_companies") override val productionCompanies: List, + @SerializedName("status") override val status: String, + @SerializedName("tagline") override val tagline: String?, + @SerializedName("created_by") val createdBy: List, + @SerializedName("first_air_date") val firstAirDate: String, + @SerializedName("in_production") val inProduction: Boolean, + @SerializedName("networks") val networks: List, + @SerializedName("number_of_episodes") val numberOfEpisodes: Int, + @SerializedName("number_of_seasons") val numberOfSeasons: Int, + @SerializedName("seasons") val seasons: List +): DetailedItem(id, title, posterPath, backdropPath, genres, overview, productionCompanies, status, tagline) \ No newline at end of file diff --git a/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/model/Genre.kt b/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/model/Genre.kt new file mode 100644 index 0000000..ba28fcf --- /dev/null +++ b/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/model/Genre.kt @@ -0,0 +1,8 @@ +package com.owenlejeune.tvtime.api.tmdb.model + +import com.google.gson.annotations.SerializedName + +data class Genre( + @SerializedName("id") val id: Int, + @SerializedName("name") val name: String +) diff --git a/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/model/Network.kt b/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/model/Network.kt new file mode 100644 index 0000000..e83315b --- /dev/null +++ b/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/model/Network.kt @@ -0,0 +1,10 @@ +package com.owenlejeune.tvtime.api.tmdb.model + +import com.google.gson.annotations.SerializedName + +data class Network( + @SerializedName("id") val id: Int, + @SerializedName("name") val name: String, + @SerializedName("logo_path") val logoPath: String?, + @SerializedName("origin_country") val originCountry: String +) diff --git a/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/model/Person.kt b/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/model/Person.kt new file mode 100644 index 0000000..744cd6e --- /dev/null +++ b/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/model/Person.kt @@ -0,0 +1,10 @@ +package com.owenlejeune.tvtime.api.tmdb.model + +import com.google.gson.annotations.SerializedName + +open class Person( + @SerializedName("id") val id: Int, + @SerializedName("credit_id") val creditId: Int, + @SerializedName("name") val name: String, + @SerializedName("gender") val gender: Int +) diff --git a/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/model/ProductionCompany.kt b/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/model/ProductionCompany.kt new file mode 100644 index 0000000..7a44a16 --- /dev/null +++ b/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/model/ProductionCompany.kt @@ -0,0 +1,9 @@ +package com.owenlejeune.tvtime.api.tmdb.model + +import com.google.gson.annotations.SerializedName + +data class ProductionCompany( + @SerializedName("id") val id: Int, + @SerializedName("name") val name: String, + @SerializedName("logo_path") val logoPath: String? +) diff --git a/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/model/Season.kt b/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/model/Season.kt new file mode 100644 index 0000000..7df0600 --- /dev/null +++ b/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/model/Season.kt @@ -0,0 +1,13 @@ +package com.owenlejeune.tvtime.api.tmdb.model + +import com.google.gson.annotations.SerializedName + +data class Season( + @SerializedName("id") val id: Int, + @SerializedName("air_date") val airDate: String, + @SerializedName("episode_count") val episodeCount: Int, + @SerializedName("name") val name: String, + @SerializedName("overview") val overview: String, + @SerializedName("poster_path") val posterPath: String, + @SerializedName("season_number") val seasonNumber: Int +) diff --git a/app/src/main/java/com/owenlejeune/tvtime/extensions/ComposeExtensions.kt b/app/src/main/java/com/owenlejeune/tvtime/extensions/ComposeExtensions.kt index 490d735..7077421 100644 --- a/app/src/main/java/com/owenlejeune/tvtime/extensions/ComposeExtensions.kt +++ b/app/src/main/java/com/owenlejeune/tvtime/extensions/ComposeExtensions.kt @@ -1,10 +1,12 @@ package com.owenlejeune.tvtime.extensions import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.lazy.LazyGridScope import androidx.compose.foundation.lazy.LazyItemScope import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.Color import androidx.paging.compose.LazyPagingItems @OptIn(ExperimentalFoundationApi::class) @@ -34,4 +36,9 @@ fun LazyListScope.listItems( items(items.size) { index -> itemContent(items[index]) } +} + +@Composable +fun Color.unlessDarkMode(other: Color): Color { + return if (isSystemInDarkTheme()) this else other } \ No newline at end of file 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 06f0a97..416c6c4 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 @@ -2,23 +2,24 @@ package com.owenlejeune.tvtime.ui.components import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.Image +import androidx.compose.foundation.background import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.GridCells import androidx.compose.foundation.lazy.LazyVerticalGrid -import androidx.compose.runtime.Composable -import androidx.compose.runtime.MutableState -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember +import androidx.compose.runtime.* import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.painterResource import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.dp import androidx.core.content.ContextCompat -import androidx.navigation.NavController import coil.compose.rememberImagePainter import coil.transform.RoundedCornersTransformation import com.owenlejeune.tvtime.R @@ -26,8 +27,6 @@ import com.owenlejeune.tvtime.api.tmdb.TmdbUtils import com.owenlejeune.tvtime.api.tmdb.model.TmdbItem import com.owenlejeune.tvtime.extensions.dpToPx import com.owenlejeune.tvtime.extensions.listItems -import com.owenlejeune.tvtime.ui.navigation.MainNavItem -import com.owenlejeune.tvtime.ui.screens.DetailViewType @OptIn(ExperimentalFoundationApi::class) @Composable @@ -83,4 +82,44 @@ fun PosterItem( } } ) +} + +@Composable +fun BackdropImage( + modifier: Modifier = Modifier, + imageUrl: String? +) { + val context = LocalContext.current + + var sizeImage by remember { mutableStateOf(IntSize.Zero) } + + val gradient = Brush.verticalGradient( + colors = listOf(Color.Transparent, Color.Black), + startY = sizeImage.height.toFloat() / 3, + endY = sizeImage.height.toFloat() + ) + + Box( + modifier = modifier + ) { + Image( + painter = if (imageUrl != null) { + rememberImagePainter( + data = imageUrl, + builder = { + placeholder(R.drawable.placeholder) + } + ) + } else { + rememberImagePainter(ContextCompat.getDrawable(context, R.drawable.placeholder)) + }, + contentDescription = "", + modifier = Modifier.onGloballyPositioned { + sizeImage = it.size + } + ) + Box( + modifier = Modifier.matchParentSize().background(gradient) + ) + } } \ 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 f9a7a42..1aaf848 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 @@ -1,18 +1,30 @@ package com.owenlejeune.tvtime.ui.screens -import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ArrowBack +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.MutableState import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.constraintlayout.compose.ConstraintLayout +import androidx.core.view.WindowCompat import androidx.navigation.NavController 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.api.tmdb.TvService import com.owenlejeune.tvtime.api.tmdb.model.DetailedItem +import com.owenlejeune.tvtime.ui.components.BackdropImage import com.owenlejeune.tvtime.ui.components.PosterItem import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -25,6 +37,8 @@ fun DetailView( itemId: Int?, type: DetailViewType ) { + val context = LocalContext.current + val mediaItem = remember { mutableStateOf(null) } val service = when(type) { DetailViewType.MOVIE -> MoviesService() @@ -35,21 +49,69 @@ fun DetailView( } ConstraintLayout( - modifier = Modifier.fillMaxSize() + modifier = Modifier + .fillMaxSize() + .background(color = MaterialTheme.colorScheme.background) ) { val ( + backButton, + backdropImage, posterImage, title ) = createRefs() + BackdropImage( + modifier = Modifier + .constrainAs(backdropImage) { + top.linkTo(parent.top) + start.linkTo(parent.start) + } + .fillMaxWidth() + .size(0.dp, 280.dp), + imageUrl = TmdbUtils.getFullBackdropPath(mediaItem.value) + ) + PosterItem( mediaItem = mediaItem.value, modifier = Modifier .constrainAs(posterImage) { - top.linkTo(parent.top, margin = 16.dp) + bottom.linkTo(title.top, margin = 8.dp) start.linkTo(parent.start, margin = 16.dp) + top.linkTo(backButton.bottom) } ) + + Text( + text = mediaItem.value?.title ?: "", + color = MaterialTheme.colorScheme.primary, + modifier = Modifier + .constrainAs(title) { + bottom.linkTo(backdropImage.bottom, margin = 8.dp) + start.linkTo(parent.start, margin = 16.dp) + end.linkTo(parent.end, margin = 16.dp) + } + .padding(start = 16.dp, end = 16.dp) + .fillMaxWidth(), + style = MaterialTheme.typography.titleLarge, + textAlign = TextAlign.Start, + softWrap = true + ) + + IconButton( + onClick = { appNavController.popBackStack() }, + modifier = Modifier + .constrainAs(backButton) { + top.linkTo(parent.top, 8.dp) + start.linkTo(parent.start, 12.dp) + } + .wrapContentSize() + ) { + Icon( + imageVector = Icons.Filled.ArrowBack, + contentDescription = "Back", + tint = MaterialTheme.colorScheme.primary + ) + } } } diff --git a/app/src/main/res/drawable/ic_back.xml b/app/src/main/res/drawable/ic_back.xml new file mode 100644 index 0000000..2a31b2e --- /dev/null +++ b/app/src/main/res/drawable/ic_back.xml @@ -0,0 +1,11 @@ + + +