display header information on details view

This commit is contained in:
Owen LeJeune
2022-02-15 09:57:31 -05:00
parent 378b152008
commit d1bd431d19
13 changed files with 232 additions and 20 deletions

View File

@@ -1,15 +1,24 @@
package com.owenlejeune.tvtime.api.tmdb package com.owenlejeune.tvtime.api.tmdb
import com.owenlejeune.tvtime.api.tmdb.model.DetailedItem
import com.owenlejeune.tvtime.api.tmdb.model.TmdbItem import com.owenlejeune.tvtime.api.tmdb.model.TmdbItem
object TmdbUtils { object TmdbUtils {
fun getFullPosterPath(posterPath: String?): String { fun getFullPosterPath(posterPath: String?): String? {
return "https://image.tmdb.org/t/p/original${posterPath}" return posterPath?.let { "https://image.tmdb.org/t/p/original${posterPath}" }
} }
fun getFullPosterPath(tmdbItem: TmdbItem): String { fun getFullPosterPath(tmdbItem: TmdbItem?): String? {
return getFullPosterPath(tmdbItem.posterPath) 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) }
} }
} }

View File

@@ -1,3 +1,13 @@
package com.owenlejeune.tvtime.api.tmdb.model package com.owenlejeune.tvtime.api.tmdb.model
abstract class DetailedItem(id: Int, title: String, posterPath: String?): TmdbItem(id, title, posterPath) abstract class DetailedItem(
id: Int,
title: String,
posterPath: String?,
@Transient open val backdropPath: String?,
@Transient open val genres: List<Genre>,
@Transient open val overview: String?,
@Transient open val productionCompanies: List<ProductionCompany>,
@Transient open val status: String,
@Transient open val tagline: String?
): TmdbItem(id, title, posterPath)

View File

@@ -5,5 +5,16 @@ import com.google.gson.annotations.SerializedName
class DetailedMovie( class DetailedMovie(
@SerializedName("id") override val id: Int, @SerializedName("id") override val id: Int,
@SerializedName("original_title") override val title: String, @SerializedName("original_title") override val title: String,
@SerializedName("poster_path") override val posterPath: String? @SerializedName("poster_path") override val posterPath: String?,
): DetailedItem(id, title, posterPath) @SerializedName("backdrop_path") override val backdropPath: String?,
@SerializedName("genres") override val genres: List<Genre>,
@SerializedName("overview") override val overview: String?,
@SerializedName("production_companies") override val productionCompanies: List<ProductionCompany>,
@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)

View File

@@ -5,5 +5,18 @@ import com.google.gson.annotations.SerializedName
class DetailedTv( class DetailedTv(
@SerializedName("id") override val id: Int, @SerializedName("id") override val id: Int,
@SerializedName("name") override val title: String, @SerializedName("name") override val title: String,
@SerializedName("poster_path") override val posterPath: String? @SerializedName("poster_path") override val posterPath: String?,
): DetailedItem(id, title, posterPath) @SerializedName("backdrop_path") override val backdropPath: String?,
@SerializedName("genres") override val genres: List<Genre>,
@SerializedName("overview") override val overview: String?,
@SerializedName("production_companies") override val productionCompanies: List<ProductionCompany>,
@SerializedName("status") override val status: String,
@SerializedName("tagline") override val tagline: String?,
@SerializedName("created_by") val createdBy: List<Person>,
@SerializedName("first_air_date") val firstAirDate: String,
@SerializedName("in_production") val inProduction: Boolean,
@SerializedName("networks") val networks: List<Network>,
@SerializedName("number_of_episodes") val numberOfEpisodes: Int,
@SerializedName("number_of_seasons") val numberOfSeasons: Int,
@SerializedName("seasons") val seasons: List<Season>
): DetailedItem(id, title, posterPath, backdropPath, genres, overview, productionCompanies, status, tagline)

View File

@@ -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
)

View File

@@ -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
)

View File

@@ -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
)

View File

@@ -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?
)

View File

@@ -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
)

View File

@@ -1,10 +1,12 @@
package com.owenlejeune.tvtime.extensions package com.owenlejeune.tvtime.extensions
import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.lazy.LazyGridScope import androidx.compose.foundation.lazy.LazyGridScope
import androidx.compose.foundation.lazy.LazyItemScope import androidx.compose.foundation.lazy.LazyItemScope
import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.Color
import androidx.paging.compose.LazyPagingItems import androidx.paging.compose.LazyPagingItems
@OptIn(ExperimentalFoundationApi::class) @OptIn(ExperimentalFoundationApi::class)
@@ -34,4 +36,9 @@ fun <T: Any> LazyListScope.listItems(
items(items.size) { index -> items(items.size) { index ->
itemContent(items[index]) itemContent(items[index])
} }
}
@Composable
fun Color.unlessDarkMode(other: Color): Color {
return if (isSystemInDarkTheme()) this else other
} }

View File

@@ -2,23 +2,24 @@ package com.owenlejeune.tvtime.ui.components
import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.Image import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.GridCells import androidx.compose.foundation.lazy.GridCells
import androidx.compose.foundation.lazy.LazyVerticalGrid import androidx.compose.foundation.lazy.LazyVerticalGrid
import androidx.compose.runtime.Composable import androidx.compose.runtime.*
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier 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.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.navigation.NavController
import coil.compose.rememberImagePainter import coil.compose.rememberImagePainter
import coil.transform.RoundedCornersTransformation import coil.transform.RoundedCornersTransformation
import com.owenlejeune.tvtime.R 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.api.tmdb.model.TmdbItem
import com.owenlejeune.tvtime.extensions.dpToPx import com.owenlejeune.tvtime.extensions.dpToPx
import com.owenlejeune.tvtime.extensions.listItems import com.owenlejeune.tvtime.extensions.listItems
import com.owenlejeune.tvtime.ui.navigation.MainNavItem
import com.owenlejeune.tvtime.ui.screens.DetailViewType
@OptIn(ExperimentalFoundationApi::class) @OptIn(ExperimentalFoundationApi::class)
@Composable @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)
)
}
} }

View File

@@ -1,18 +1,30 @@
package com.owenlejeune.tvtime.ui.screens 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.Composable
import androidx.compose.runtime.MutableState import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier 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.compose.ui.unit.dp
import androidx.constraintlayout.compose.ConstraintLayout import androidx.constraintlayout.compose.ConstraintLayout
import androidx.core.view.WindowCompat
import androidx.navigation.NavController import androidx.navigation.NavController
import com.owenlejeune.tvtime.api.tmdb.DetailService import com.owenlejeune.tvtime.api.tmdb.DetailService
import com.owenlejeune.tvtime.api.tmdb.MoviesService 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.TvService
import com.owenlejeune.tvtime.api.tmdb.model.DetailedItem import com.owenlejeune.tvtime.api.tmdb.model.DetailedItem
import com.owenlejeune.tvtime.ui.components.BackdropImage
import com.owenlejeune.tvtime.ui.components.PosterItem import com.owenlejeune.tvtime.ui.components.PosterItem
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@@ -25,6 +37,8 @@ fun DetailView(
itemId: Int?, itemId: Int?,
type: DetailViewType type: DetailViewType
) { ) {
val context = LocalContext.current
val mediaItem = remember { mutableStateOf<DetailedItem?>(null) } val mediaItem = remember { mutableStateOf<DetailedItem?>(null) }
val service = when(type) { val service = when(type) {
DetailViewType.MOVIE -> MoviesService() DetailViewType.MOVIE -> MoviesService()
@@ -35,21 +49,69 @@ fun DetailView(
} }
ConstraintLayout( ConstraintLayout(
modifier = Modifier.fillMaxSize() modifier = Modifier
.fillMaxSize()
.background(color = MaterialTheme.colorScheme.background)
) { ) {
val ( val (
backButton,
backdropImage,
posterImage, posterImage,
title title
) = createRefs() ) = 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( PosterItem(
mediaItem = mediaItem.value, mediaItem = mediaItem.value,
modifier = Modifier modifier = Modifier
.constrainAs(posterImage) { .constrainAs(posterImage) {
top.linkTo(parent.top, margin = 16.dp) bottom.linkTo(title.top, margin = 8.dp)
start.linkTo(parent.start, margin = 16.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
)
}
} }
} }

View File

@@ -0,0 +1,11 @@
<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"
android:autoMirrored="true">
<path
android:fillColor="@android:color/white"
android:pathData="M20,11H7.83l5.59,-5.59L12,4l-8,8 8,8 1.41,-1.41L7.83,13H20v-2z"/>
</vector>