mirror of
https://github.com/owenlejeune/TVTime.git
synced 2025-11-23 04:00:53 -05:00
display header information on details view
This commit is contained in:
@@ -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) }
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -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)
|
||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
@@ -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
|
||||||
|
)
|
||||||
@@ -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
|
||||||
|
)
|
||||||
@@ -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
|
||||||
|
)
|
||||||
@@ -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?
|
||||||
|
)
|
||||||
@@ -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
|
||||||
|
)
|
||||||
@@ -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
|
||||||
}
|
}
|
||||||
@@ -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)
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -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
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
11
app/src/main/res/drawable/ic_back.xml
Normal file
11
app/src/main/res/drawable/ic_back.xml
Normal 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>
|
||||||
Reference in New Issue
Block a user