mirror of
https://github.com/owenlejeune/TVTime.git
synced 2025-11-22 11:40:54 -05:00
refactor some detail view code
This commit is contained in:
@@ -7,12 +7,11 @@ class DetailCast(
|
|||||||
@SerializedName("id") val id: Int,
|
@SerializedName("id") val id: Int,
|
||||||
@SerializedName("episode_count") val episodeCount: Int,
|
@SerializedName("episode_count") val episodeCount: Int,
|
||||||
@SerializedName("overview") val overview: String,
|
@SerializedName("overview") val overview: String,
|
||||||
@SerializedName("name") val name: String?,
|
@SerializedName("name", alternate = ["title"]) val name: String,
|
||||||
@SerializedName("media_type") val mediaType: MediaViewType,
|
@SerializedName("media_type") val mediaType: MediaViewType,
|
||||||
@SerializedName("poster_path") val posterPath: String?,
|
@SerializedName("poster_path") val posterPath: String?,
|
||||||
@SerializedName("first_air_date") val firstAirDate: String,
|
@SerializedName("first_air_date") val firstAirDate: String,
|
||||||
@SerializedName("character") val character: String,
|
@SerializedName("character") val character: String,
|
||||||
@SerializedName("title") val title: String?,
|
|
||||||
@SerializedName("adult") val isAdult: Boolean,
|
@SerializedName("adult") val isAdult: Boolean,
|
||||||
@SerializedName("release_date") val releaseDate: String
|
@SerializedName("release_date") val releaseDate: String
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -156,7 +156,8 @@ fun PosterItem(
|
|||||||
fun BackdropImage(
|
fun BackdropImage(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
imageUrl: String? = null,
|
imageUrl: String? = null,
|
||||||
collection: ImageCollection? = null
|
collection: ImageCollection? = null,
|
||||||
|
contentDescription: String? = null
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
|
||||||
@@ -200,7 +201,7 @@ fun BackdropImage(
|
|||||||
} else {
|
} else {
|
||||||
rememberImagePainter(ContextCompat.getDrawable(context, R.drawable.placeholder))
|
rememberImagePainter(ContextCompat.getDrawable(context, R.drawable.placeholder))
|
||||||
},
|
},
|
||||||
contentDescription = "",
|
contentDescription = contentDescription,
|
||||||
modifier = Modifier.onGloballyPositioned {
|
modifier = Modifier.onGloballyPositioned {
|
||||||
sizeImage = it.size
|
sizeImage = it.size
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import androidx.navigation.NavType
|
|||||||
import androidx.navigation.compose.NavHost
|
import androidx.navigation.compose.NavHost
|
||||||
import androidx.navigation.compose.composable
|
import androidx.navigation.compose.composable
|
||||||
import androidx.navigation.navArgument
|
import androidx.navigation.navArgument
|
||||||
import com.owenlejeune.tvtime.ui.screens.DetailView
|
import com.owenlejeune.tvtime.ui.screens.MediaDetailView
|
||||||
import com.owenlejeune.tvtime.ui.screens.MainAppView
|
import com.owenlejeune.tvtime.ui.screens.MainAppView
|
||||||
import com.owenlejeune.tvtime.ui.screens.MediaViewType
|
import com.owenlejeune.tvtime.ui.screens.MediaViewType
|
||||||
import com.owenlejeune.tvtime.ui.screens.PersonDetailView
|
import com.owenlejeune.tvtime.ui.screens.PersonDetailView
|
||||||
@@ -40,7 +40,7 @@ fun MainNavigationRoutes(navController: NavHostController, displayUnderStatusBar
|
|||||||
val args = navBackStackEntry.arguments
|
val args = navBackStackEntry.arguments
|
||||||
val mediaType = args?.getSerializable(NavConstants.TYPE_KEY) as MediaViewType
|
val mediaType = args?.getSerializable(NavConstants.TYPE_KEY) as MediaViewType
|
||||||
if (mediaType != MediaViewType.PERSON) {
|
if (mediaType != MediaViewType.PERSON) {
|
||||||
DetailView(
|
MediaDetailView(
|
||||||
appNavController = navController,
|
appNavController = navController,
|
||||||
itemId = args.getInt(NavConstants.ID_KEY),
|
itemId = args.getInt(NavConstants.ID_KEY),
|
||||||
type = mediaType
|
type = mediaType
|
||||||
|
|||||||
@@ -0,0 +1,193 @@
|
|||||||
|
package com.owenlejeune.tvtime.ui.screens
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.isSystemInDarkTheme
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
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.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.graphics.Brush
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import androidx.constraintlayout.compose.ConstraintLayout
|
||||||
|
import androidx.navigation.NavController
|
||||||
|
import com.owenlejeune.tvtime.R
|
||||||
|
import com.owenlejeune.tvtime.ui.components.BackdropImage
|
||||||
|
import com.owenlejeune.tvtime.ui.components.PosterItem
|
||||||
|
import com.owenlejeune.tvtime.ui.components.RatingRing
|
||||||
|
import com.owenlejeune.tvtime.utils.TmdbUtils
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun DetailContent(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
content: @Composable () -> Unit
|
||||||
|
) {
|
||||||
|
Box(modifier = modifier
|
||||||
|
.background(color = MaterialTheme.colorScheme.background)
|
||||||
|
.verticalScroll(rememberScrollState())
|
||||||
|
) {
|
||||||
|
content()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun DetailHeader(
|
||||||
|
appNavController: NavController,
|
||||||
|
title: String,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
backdropUrl: String? = null,
|
||||||
|
posterUrl: String? = null,
|
||||||
|
backdropContentDescription: String? = null,
|
||||||
|
posterContentDescription: String? = null,
|
||||||
|
rating: Float? = null
|
||||||
|
) {
|
||||||
|
ConstraintLayout(modifier = modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.wrapContentHeight()
|
||||||
|
) {
|
||||||
|
val (
|
||||||
|
backButton, backdropImage, posterImage, titleText, ratingsView
|
||||||
|
) = createRefs()
|
||||||
|
|
||||||
|
Backdrop(
|
||||||
|
modifier = Modifier
|
||||||
|
.constrainAs(backdropImage) {
|
||||||
|
top.linkTo(parent.top)
|
||||||
|
start.linkTo(parent.start)
|
||||||
|
end.linkTo(parent.end)
|
||||||
|
},
|
||||||
|
imageUrl = backdropUrl,
|
||||||
|
contentDescription = backdropContentDescription
|
||||||
|
)
|
||||||
|
|
||||||
|
PosterItem(
|
||||||
|
modifier = Modifier
|
||||||
|
.constrainAs(posterImage) {
|
||||||
|
bottom.linkTo(backdropImage.bottom)
|
||||||
|
start.linkTo(parent.start)
|
||||||
|
top.linkTo(backButton.bottom)
|
||||||
|
},
|
||||||
|
url = posterUrl,
|
||||||
|
contentDescription = posterContentDescription
|
||||||
|
)
|
||||||
|
|
||||||
|
TitleText(
|
||||||
|
modifier = Modifier
|
||||||
|
.constrainAs(titleText) {
|
||||||
|
bottom.linkTo(posterImage.bottom)
|
||||||
|
start.linkTo(posterImage.end, margin = 8.dp)
|
||||||
|
end.linkTo(parent.end)
|
||||||
|
},
|
||||||
|
title = title
|
||||||
|
)
|
||||||
|
|
||||||
|
rating?.let {
|
||||||
|
RatingView(
|
||||||
|
modifier = Modifier
|
||||||
|
.constrainAs(ratingsView) {
|
||||||
|
bottom.linkTo(titleText.top)
|
||||||
|
start.linkTo(posterImage.end, margin = 20.dp)
|
||||||
|
},
|
||||||
|
progress = rating
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
BackButton(
|
||||||
|
modifier = Modifier.constrainAs(backButton) {
|
||||||
|
top.linkTo(parent.top)//, 8.dp)
|
||||||
|
start.linkTo(parent.start)//, 12.dp)
|
||||||
|
bottom.linkTo(posterImage.top)
|
||||||
|
},
|
||||||
|
appNavController = appNavController
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun Backdrop(modifier: Modifier, imageUrl: String?, contentDescription: String? = null) {
|
||||||
|
// val images = remember { mutableStateOf<ImageCollection?>(null) }
|
||||||
|
// itemId?.let {
|
||||||
|
// if (images.value == null) {
|
||||||
|
// fetchImages(itemId, service, images)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
BackdropImage(
|
||||||
|
modifier = modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(280.dp),
|
||||||
|
imageUrl = TmdbUtils.getFullBackdropPath(imageUrl),
|
||||||
|
contentDescription = contentDescription
|
||||||
|
// collection = images.value
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun TitleText(modifier: Modifier, title: String) {
|
||||||
|
Text(
|
||||||
|
text = title,
|
||||||
|
color = MaterialTheme.colorScheme.primary,
|
||||||
|
modifier = modifier
|
||||||
|
.padding(start = 16.dp, end = 16.dp)
|
||||||
|
.fillMaxWidth(.6f),
|
||||||
|
style = MaterialTheme.typography.headlineMedium,
|
||||||
|
textAlign = TextAlign.Start,
|
||||||
|
softWrap = true,
|
||||||
|
maxLines = 3,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun RatingView(
|
||||||
|
progress: Float,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = modifier
|
||||||
|
.clip(CircleShape)
|
||||||
|
.size(60.dp)
|
||||||
|
.background(color = MaterialTheme.colorScheme.surfaceVariant)
|
||||||
|
) {
|
||||||
|
RatingRing(
|
||||||
|
modifier = Modifier.padding(5.dp),
|
||||||
|
textColor = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
progress = progress,
|
||||||
|
textSize = 14.sp,
|
||||||
|
ringColor = MaterialTheme.colorScheme.primary,
|
||||||
|
ringStrokeWidth = 4.dp,
|
||||||
|
size = 50.dp
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun BackButton(modifier: Modifier, appNavController: NavController) {
|
||||||
|
val start = if (isSystemInDarkTheme()) Color.Black else Color.White
|
||||||
|
IconButton(
|
||||||
|
onClick = { appNavController.popBackStack() },
|
||||||
|
modifier = modifier
|
||||||
|
.background(
|
||||||
|
brush = Brush.radialGradient(colors = listOf(start, Color.Transparent))
|
||||||
|
)
|
||||||
|
.wrapContentSize()
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Filled.ArrowBack,
|
||||||
|
contentDescription = stringResource(R.string.content_description_back_button),
|
||||||
|
tint = MaterialTheme.colorScheme.primary
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,21 +1,17 @@
|
|||||||
package com.owenlejeune.tvtime.ui.screens
|
package com.owenlejeune.tvtime.ui.screens
|
||||||
|
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.compose.foundation.*
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.lazy.LazyRow
|
import androidx.compose.foundation.lazy.LazyRow
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.ArrowBack
|
|
||||||
import androidx.compose.material.icons.filled.Delete
|
import androidx.compose.material.icons.filled.Delete
|
||||||
import androidx.compose.material.icons.filled.Send
|
import androidx.compose.material.icons.filled.Send
|
||||||
import androidx.compose.material3.*
|
import androidx.compose.material3.*
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
|
||||||
import androidx.compose.ui.graphics.Brush
|
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.ColorFilter
|
import androidx.compose.ui.graphics.ColorFilter
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
@@ -23,15 +19,12 @@ import androidx.compose.ui.res.stringResource
|
|||||||
import androidx.compose.ui.text.font.FontStyle
|
import androidx.compose.ui.text.font.FontStyle
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import androidx.constraintlayout.compose.ConstraintLayout
|
|
||||||
import androidx.navigation.NavController
|
import androidx.navigation.NavController
|
||||||
import com.owenlejeune.tvtime.R
|
import com.owenlejeune.tvtime.R
|
||||||
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.PeopleService
|
|
||||||
import com.owenlejeune.tvtime.api.tmdb.TvService
|
import com.owenlejeune.tvtime.api.tmdb.TvService
|
||||||
import com.owenlejeune.tvtime.api.tmdb.model.*
|
import com.owenlejeune.tvtime.api.tmdb.model.*
|
||||||
import com.owenlejeune.tvtime.extensions.listItems
|
import com.owenlejeune.tvtime.extensions.listItems
|
||||||
@@ -47,7 +40,7 @@ import org.json.JSONObject
|
|||||||
import java.text.DecimalFormat
|
import java.text.DecimalFormat
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun DetailView(
|
fun MediaDetailView(
|
||||||
appNavController: NavController,
|
appNavController: NavController,
|
||||||
itemId: Int?,
|
itemId: Int?,
|
||||||
type: MediaViewType
|
type: MediaViewType
|
||||||
@@ -65,349 +58,25 @@ fun DetailView(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val scrollState = rememberScrollState()
|
DetailContent(
|
||||||
|
modifier = Modifier.fillMaxSize()
|
||||||
ConstraintLayout(
|
) {
|
||||||
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.background(color = MaterialTheme.colorScheme.background)
|
|
||||||
.verticalScroll(state = scrollState)
|
|
||||||
) {
|
|
||||||
val (
|
|
||||||
backButton, backdropImage, posterImage, titleText, contentColumn, ratingsView
|
|
||||||
) = createRefs()
|
|
||||||
|
|
||||||
Backdrop(
|
|
||||||
modifier = Modifier.constrainAs(backdropImage) {
|
|
||||||
top.linkTo(parent.top)
|
|
||||||
start.linkTo(parent.start)
|
|
||||||
end.linkTo(parent.end)
|
|
||||||
},
|
|
||||||
mediaItem = mediaItem
|
|
||||||
)
|
|
||||||
|
|
||||||
PosterItem(
|
|
||||||
mediaItem = mediaItem.value,
|
|
||||||
modifier = Modifier
|
|
||||||
.constrainAs(posterImage) {
|
|
||||||
bottom.linkTo(backdropImage.bottom)
|
|
||||||
start.linkTo(parent.start, margin = 16.dp)
|
|
||||||
top.linkTo(backButton.bottom)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
TitleText(
|
|
||||||
modifier = Modifier.constrainAs(titleText) {
|
|
||||||
bottom.linkTo(posterImage.bottom)
|
|
||||||
start.linkTo(posterImage.end, margin = 8.dp)
|
|
||||||
end.linkTo(parent.end, margin = 16.dp)
|
|
||||||
},
|
|
||||||
title = mediaItem.value?.title ?: "",
|
|
||||||
)
|
|
||||||
|
|
||||||
RatingView(
|
|
||||||
modifier = Modifier
|
|
||||||
.constrainAs(ratingsView) {
|
|
||||||
bottom.linkTo(titleText.top)
|
|
||||||
start.linkTo(posterImage.end, margin = 20.dp)
|
|
||||||
},
|
|
||||||
progress = mediaItem.value?.voteAverage?.let { it / 10 } ?: 0f
|
|
||||||
)
|
|
||||||
|
|
||||||
BackButton(
|
|
||||||
modifier = Modifier.constrainAs(backButton) {
|
|
||||||
top.linkTo(parent.top)//, 8.dp)
|
|
||||||
start.linkTo(parent.start, 12.dp)
|
|
||||||
bottom.linkTo(posterImage.top)
|
|
||||||
},
|
|
||||||
appNavController = appNavController
|
|
||||||
)
|
|
||||||
|
|
||||||
ContentColumn(
|
|
||||||
modifier = Modifier.constrainAs(contentColumn) {
|
|
||||||
top.linkTo(backdropImage.bottom)//, margin = 8.dp)
|
|
||||||
},
|
|
||||||
itemId = itemId,
|
|
||||||
mediaItem = mediaItem,
|
|
||||||
service = service,
|
|
||||||
mediaType = type,
|
|
||||||
appNavController = appNavController
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun PersonDetailView(
|
|
||||||
appNavController: NavController,
|
|
||||||
personId: Int?
|
|
||||||
) {
|
|
||||||
val person = remember { mutableStateOf<DetailPerson?>(null) }
|
|
||||||
personId?.let {
|
|
||||||
if (person.value == null) {
|
|
||||||
fetchPerson(personId, person)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val scrollState = rememberScrollState()
|
|
||||||
|
|
||||||
ConstraintLayout(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
.background(color = MaterialTheme.colorScheme.background)
|
|
||||||
.verticalScroll(state = scrollState)
|
|
||||||
) {
|
|
||||||
val (
|
|
||||||
backButton, backdropImage, profileImage, nameText, contentColumn
|
|
||||||
) = createRefs()
|
|
||||||
|
|
||||||
BackdropImage(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.height(280.dp)
|
|
||||||
.constrainAs(backdropImage) {
|
|
||||||
top.linkTo(parent.top)
|
|
||||||
start.linkTo(parent.start)
|
|
||||||
end.linkTo(parent.end)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
PosterItem(
|
|
||||||
person = person.value,
|
|
||||||
modifier = Modifier
|
|
||||||
.constrainAs(profileImage) {
|
|
||||||
bottom.linkTo(backdropImage.bottom)
|
|
||||||
start.linkTo(parent.start, margin = 16.dp)
|
|
||||||
top.linkTo(backButton.bottom)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
TitleText(
|
|
||||||
modifier = Modifier.constrainAs(nameText) {
|
|
||||||
bottom.linkTo(profileImage.bottom)
|
|
||||||
start.linkTo(profileImage.end, margin = 8.dp)
|
|
||||||
end.linkTo(parent.end, margin = 16.dp)
|
|
||||||
},
|
|
||||||
title = person.value?.name ?: ""
|
|
||||||
)
|
|
||||||
|
|
||||||
BackButton(
|
|
||||||
modifier = Modifier.constrainAs(backButton) {
|
|
||||||
top.linkTo(parent.top)
|
|
||||||
start.linkTo(parent.start, 12.dp)
|
|
||||||
bottom.linkTo(profileImage.top)
|
|
||||||
},
|
|
||||||
appNavController = appNavController
|
|
||||||
)
|
|
||||||
|
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.wrapContentHeight()
|
|
||||||
.padding(start = 16.dp, end = 16.dp, bottom = 16.dp)
|
|
||||||
.constrainAs(contentColumn) {
|
|
||||||
top.linkTo(backdropImage.bottom)//, margin = 8.dp)
|
|
||||||
},
|
|
||||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
|
||||||
) {
|
|
||||||
ExpandableContentCard { isExpanded ->
|
|
||||||
Text(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.wrapContentHeight()
|
|
||||||
.padding(top = 12.dp, start = 16.dp, end = 16.dp),
|
|
||||||
text = person.value?.biography ?: "",
|
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
|
||||||
maxLines = if (isExpanded) Int.MAX_VALUE else 3,
|
|
||||||
overflow = TextOverflow.Ellipsis
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
val credits = remember { mutableStateOf<PersonCreditsResponse?>(null) }
|
|
||||||
personId?.let {
|
|
||||||
if (credits.value == null) {
|
|
||||||
fetchCredits(personId, credits)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ContentCard(title = stringResource(R.string.known_for_label)) {
|
|
||||||
LazyRow(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.wrapContentHeight()
|
|
||||||
.padding(12.dp),
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(4.dp)
|
|
||||||
) {
|
|
||||||
items(credits.value?.cast?.size ?: 0) { i ->
|
|
||||||
val content = credits.value!!.cast[i]
|
|
||||||
|
|
||||||
val title = if (content.mediaType == MediaViewType.MOVIE) {
|
|
||||||
content.title ?: ""
|
|
||||||
} else {
|
|
||||||
content.name ?: ""
|
|
||||||
}
|
|
||||||
TwoLineImageTextCard(
|
|
||||||
title = title,
|
|
||||||
subtitle = content.character,
|
|
||||||
modifier = Modifier
|
|
||||||
.width(124.dp)
|
|
||||||
.wrapContentHeight(),
|
|
||||||
imageUrl = TmdbUtils.getFullPosterPath(content.posterPath),
|
|
||||||
onItemClicked = {
|
|
||||||
personId?.let {
|
|
||||||
appNavController.navigate(
|
|
||||||
"${MainNavItem.DetailView.route}/${content.mediaType}/${content.id}"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ContentCard(title = stringResource(R.string.also_known_for_label)) {
|
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.wrapContentHeight()
|
|
||||||
.padding(12.dp),
|
|
||||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
|
||||||
) {
|
|
||||||
val departments = credits.value?.crew?.map { it.department }?.toSet() ?: emptySet()
|
|
||||||
if (departments.isNotEmpty()) {
|
|
||||||
departments.forEach { department ->
|
|
||||||
Text(text = department, color = MaterialTheme.colorScheme.onSurface)
|
|
||||||
LazyRow(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.wrapContentHeight(),
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(4.dp)
|
|
||||||
) {
|
|
||||||
val jobsInDepartment = credits.value!!.crew.filter { it.department == department }
|
|
||||||
items(jobsInDepartment.size) { i ->
|
|
||||||
val content = jobsInDepartment[i]
|
|
||||||
val title = if (content.mediaType == MediaViewType.MOVIE) {
|
|
||||||
content.title ?: ""
|
|
||||||
} else {
|
|
||||||
content.name ?: ""
|
|
||||||
}
|
|
||||||
TwoLineImageTextCard(
|
|
||||||
title = title,
|
|
||||||
subtitle = content.job,
|
|
||||||
modifier = Modifier
|
|
||||||
.width(124.dp)
|
|
||||||
.wrapContentHeight(),
|
|
||||||
imageUrl = TmdbUtils.getFullPosterPath(content.posterPath),
|
|
||||||
onItemClicked = {
|
|
||||||
personId?.let {
|
|
||||||
appNavController.navigate(
|
|
||||||
"${MainNavItem.DetailView.route}/${content.mediaType}/${content.id}"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun Backdrop(modifier: Modifier, mediaItem: MutableState<DetailedItem?>) {
|
|
||||||
// val images = remember { mutableStateOf<ImageCollection?>(null) }
|
|
||||||
// itemId?.let {
|
|
||||||
// if (images.value == null) {
|
|
||||||
// fetchImages(itemId, service, images)
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
BackdropImage(
|
|
||||||
modifier = modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.height(280.dp),
|
|
||||||
imageUrl = TmdbUtils.getFullBackdropPath(mediaItem.value),
|
|
||||||
// collection = images.value
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun TitleText(modifier: Modifier, title: String) {
|
|
||||||
Text(
|
|
||||||
text = title,
|
|
||||||
color = MaterialTheme.colorScheme.primary,
|
|
||||||
modifier = modifier
|
|
||||||
.padding(start = 16.dp, end = 16.dp)
|
|
||||||
.fillMaxWidth(.6f),
|
|
||||||
style = MaterialTheme.typography.headlineMedium,
|
|
||||||
textAlign = TextAlign.Start,
|
|
||||||
softWrap = true,
|
|
||||||
maxLines = 3,
|
|
||||||
overflow = TextOverflow.Ellipsis
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun BackButton(modifier: Modifier, appNavController: NavController) {
|
|
||||||
val start = if (isSystemInDarkTheme()) Color.Black else Color.White
|
|
||||||
IconButton(
|
|
||||||
onClick = { appNavController.popBackStack() },
|
|
||||||
modifier = modifier
|
|
||||||
.background(
|
|
||||||
brush = Brush.radialGradient(colors = listOf(start, Color.Transparent))
|
|
||||||
)
|
|
||||||
.wrapContentSize()
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
imageVector = Icons.Filled.ArrowBack,
|
|
||||||
contentDescription = stringResource(R.string.content_description_back_button),
|
|
||||||
tint = MaterialTheme.colorScheme.primary
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun RatingView(
|
|
||||||
progress: Float,
|
|
||||||
modifier: Modifier = Modifier
|
|
||||||
) {
|
|
||||||
Box(
|
|
||||||
modifier = modifier
|
|
||||||
.clip(CircleShape)
|
|
||||||
.size(60.dp)
|
|
||||||
.background(color = MaterialTheme.colorScheme.surfaceVariant)
|
|
||||||
) {
|
|
||||||
RatingRing(
|
|
||||||
modifier = Modifier.padding(5.dp),
|
|
||||||
textColor = MaterialTheme.colorScheme.onSurfaceVariant,
|
|
||||||
progress = progress,
|
|
||||||
textSize = 14.sp,
|
|
||||||
ringColor = MaterialTheme.colorScheme.primary,
|
|
||||||
ringStrokeWidth = 4.dp,
|
|
||||||
size = 50.dp
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun ContentColumn(
|
|
||||||
modifier: Modifier,
|
|
||||||
itemId: Int?,
|
|
||||||
mediaItem: MutableState<DetailedItem?>,
|
|
||||||
service: DetailService,
|
|
||||||
mediaType: MediaViewType,
|
|
||||||
appNavController: NavController
|
|
||||||
) {
|
|
||||||
Column(
|
|
||||||
modifier = modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.wrapContentHeight()
|
|
||||||
.padding(start = 16.dp, end = 16.dp, bottom = 16.dp),
|
.padding(start = 16.dp, end = 16.dp, bottom = 16.dp),
|
||||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||||
) {
|
) {
|
||||||
if (mediaType == MediaViewType.MOVIE) {
|
DetailHeader(
|
||||||
|
appNavController = appNavController,
|
||||||
|
title = mediaItem.value?.title ?: "",
|
||||||
|
posterUrl = TmdbUtils.getFullPosterPath(mediaItem.value?.posterPath),
|
||||||
|
posterContentDescription = mediaItem.value?.title,
|
||||||
|
backdropUrl = TmdbUtils.getFullBackdropPath(mediaItem.value?.backdropPath),
|
||||||
|
rating = mediaItem.value?.voteAverage?.let { it / 10 }
|
||||||
|
)
|
||||||
|
|
||||||
|
if (type == MediaViewType.MOVIE) {
|
||||||
MiscMovieDetails(mediaItem = mediaItem, service as MoviesService)
|
MiscMovieDetails(mediaItem = mediaItem, service as MoviesService)
|
||||||
} else {
|
} else {
|
||||||
MiscTvDetails(mediaItem = mediaItem, service as TvService)
|
MiscTvDetails(mediaItem = mediaItem, service as TvService)
|
||||||
@@ -419,15 +88,16 @@ private fun ContentColumn(
|
|||||||
|
|
||||||
CastCard(itemId = itemId, service = service, appNavController = appNavController)
|
CastCard(itemId = itemId, service = service, appNavController = appNavController)
|
||||||
|
|
||||||
SimilarContentCard(itemId = itemId, service = service, mediaType = mediaType, appNavController = appNavController)
|
SimilarContentCard(itemId = itemId, service = service, mediaType = type, appNavController = appNavController)
|
||||||
|
|
||||||
VideosCard(itemId = itemId, service = service)
|
VideosCard(itemId = itemId, service = service)
|
||||||
|
|
||||||
ActionsView(itemId = itemId, type = mediaType, service = service)
|
ActionsView(itemId = itemId, type = type, service = service)
|
||||||
|
|
||||||
ReviewsCard(itemId = itemId, service = service)
|
ReviewsCard(itemId = itemId, service = service)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun MiscTvDetails(mediaItem: MutableState<DetailedItem?>, service: TvService) {
|
private fun MiscTvDetails(mediaItem: MutableState<DetailedItem?>, service: TvService) {
|
||||||
@@ -1100,28 +770,6 @@ private fun fetchVideos(id: Int, service: DetailService, videoResponse: MutableS
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun fetchPerson(id: Int, person: MutableState<DetailPerson?>) {
|
|
||||||
CoroutineScope(Dispatchers.IO).launch {
|
|
||||||
val result = PeopleService().getPerson(id)
|
|
||||||
if (result.isSuccessful) {
|
|
||||||
withContext(Dispatchers.Main) {
|
|
||||||
person.value = result.body()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun fetchCredits(id: Int, credits: MutableState<PersonCreditsResponse?>) {
|
|
||||||
CoroutineScope(Dispatchers.IO).launch {
|
|
||||||
val result = PeopleService().getCredits(id)
|
|
||||||
if (result.isSuccessful) {
|
|
||||||
withContext(Dispatchers.Main) {
|
|
||||||
credits.value = result.body()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun fetchReviews(id: Int, service: DetailService, reviewResponse: MutableState<ReviewResponse?>) {
|
private fun fetchReviews(id: Int, service: DetailService, reviewResponse: MutableState<ReviewResponse?>) {
|
||||||
CoroutineScope(Dispatchers.IO).launch {
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
val result = service.getReviews(id)
|
val result = service.getReviews(id)
|
||||||
@@ -0,0 +1,186 @@
|
|||||||
|
package com.owenlejeune.tvtime.ui.screens
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.lazy.LazyRow
|
||||||
|
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.res.stringResource
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.navigation.NavController
|
||||||
|
import com.owenlejeune.tvtime.R
|
||||||
|
import com.owenlejeune.tvtime.api.tmdb.PeopleService
|
||||||
|
import com.owenlejeune.tvtime.api.tmdb.model.DetailPerson
|
||||||
|
import com.owenlejeune.tvtime.api.tmdb.model.PersonCreditsResponse
|
||||||
|
import com.owenlejeune.tvtime.ui.components.ContentCard
|
||||||
|
import com.owenlejeune.tvtime.ui.components.ExpandableContentCard
|
||||||
|
import com.owenlejeune.tvtime.ui.components.TwoLineImageTextCard
|
||||||
|
import com.owenlejeune.tvtime.ui.navigation.MainNavItem
|
||||||
|
import com.owenlejeune.tvtime.utils.TmdbUtils
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun PersonDetailView(
|
||||||
|
appNavController: NavController,
|
||||||
|
personId: Int?
|
||||||
|
) {
|
||||||
|
val person = remember { mutableStateOf<DetailPerson?>(null) }
|
||||||
|
personId?.let {
|
||||||
|
if (person.value == null) {
|
||||||
|
fetchPerson(personId, person)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DetailContent(
|
||||||
|
modifier = Modifier.fillMaxSize()
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(start = 16.dp, end = 16.dp, bottom = 16.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||||
|
) {
|
||||||
|
DetailHeader(
|
||||||
|
appNavController = appNavController,
|
||||||
|
title = person.value?.name ?: "",
|
||||||
|
posterUrl = TmdbUtils.getFullPersonImagePath(person.value?.profilePath),
|
||||||
|
posterContentDescription = person.value?.name
|
||||||
|
)
|
||||||
|
|
||||||
|
BiographyCard(person = person.value)
|
||||||
|
|
||||||
|
val credits = remember { mutableStateOf<PersonCreditsResponse?>(null) }
|
||||||
|
personId?.let {
|
||||||
|
if (credits.value == null) {
|
||||||
|
fetchCredits(personId, credits)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ContentCard(title = stringResource(R.string.known_for_label)) {
|
||||||
|
LazyRow(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.wrapContentHeight()
|
||||||
|
.padding(12.dp),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(4.dp)
|
||||||
|
) {
|
||||||
|
items(credits.value?.cast?.size ?: 0) { i ->
|
||||||
|
val content = credits.value!!.cast[i]
|
||||||
|
|
||||||
|
TwoLineImageTextCard(
|
||||||
|
title = content.name,
|
||||||
|
titleTextColor = MaterialTheme.colorScheme.primary,
|
||||||
|
subtitle = content.character,
|
||||||
|
modifier = Modifier
|
||||||
|
.width(124.dp)
|
||||||
|
.wrapContentHeight(),
|
||||||
|
imageUrl = TmdbUtils.getFullPosterPath(content.posterPath),
|
||||||
|
onItemClicked = {
|
||||||
|
personId?.let {
|
||||||
|
appNavController.navigate(
|
||||||
|
"${MainNavItem.DetailView.route}/${content.mediaType}/${content.id}"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ContentCard(title = stringResource(R.string.also_known_for_label)) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.wrapContentHeight()
|
||||||
|
.padding(12.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
|
) {
|
||||||
|
val departments = credits.value?.crew?.map { it.department }?.toSet() ?: emptySet()
|
||||||
|
if (departments.isNotEmpty()) {
|
||||||
|
departments.forEach { department ->
|
||||||
|
Text(text = department, color = MaterialTheme.colorScheme.primary)
|
||||||
|
LazyRow(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.wrapContentHeight(),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(4.dp)
|
||||||
|
) {
|
||||||
|
val jobsInDepartment = credits.value!!.crew.filter { it.department == department }
|
||||||
|
items(jobsInDepartment.size) { i ->
|
||||||
|
val content = jobsInDepartment[i]
|
||||||
|
val title = if (content.mediaType == MediaViewType.MOVIE) {
|
||||||
|
content.title ?: ""
|
||||||
|
} else {
|
||||||
|
content.name ?: ""
|
||||||
|
}
|
||||||
|
TwoLineImageTextCard(
|
||||||
|
title = title,
|
||||||
|
subtitle = content.job,
|
||||||
|
modifier = Modifier
|
||||||
|
.width(124.dp)
|
||||||
|
.wrapContentHeight(),
|
||||||
|
imageUrl = TmdbUtils.getFullPosterPath(content.posterPath),
|
||||||
|
onItemClicked = {
|
||||||
|
personId?.let {
|
||||||
|
appNavController.navigate(
|
||||||
|
"${MainNavItem.DetailView.route}/${content.mediaType}/${content.id}"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun BiographyCard(person: DetailPerson?) {
|
||||||
|
ExpandableContentCard { isExpanded ->
|
||||||
|
Text(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.wrapContentHeight()
|
||||||
|
.padding(top = 12.dp, start = 16.dp, end = 16.dp),
|
||||||
|
text = person?.biography ?: "",
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
maxLines = if (isExpanded) Int.MAX_VALUE else 3,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun fetchPerson(id: Int, person: MutableState<DetailPerson?>) {
|
||||||
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
|
val result = PeopleService().getPerson(id)
|
||||||
|
if (result.isSuccessful) {
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
person.value = result.body()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun fetchCredits(id: Int, credits: MutableState<PersonCreditsResponse?>) {
|
||||||
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
|
val result = PeopleService().getCredits(id)
|
||||||
|
if (result.isSuccessful) {
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
credits.value = result.body()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -83,7 +83,7 @@ fun TVTimeTheme(
|
|||||||
}
|
}
|
||||||
|
|
||||||
val systemUiController = rememberSystemUiController()
|
val systemUiController = rememberSystemUiController()
|
||||||
systemUiController.setStatusBarColor(colorScheme.background, !isDarkTheme)
|
systemUiController.setSystemBarsColor(colorScheme.background, !isDarkTheme)
|
||||||
|
|
||||||
MaterialTheme(
|
MaterialTheme(
|
||||||
colorScheme = colorScheme,
|
colorScheme = colorScheme,
|
||||||
|
|||||||
Reference in New Issue
Block a user