refactor some detail view code

This commit is contained in:
Owen LeJeune
2022-03-03 17:20:14 -05:00
parent 2605f6b3f4
commit b8b5fd1e3b
7 changed files with 418 additions and 391 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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