refactor media detail view

This commit is contained in:
Owen LeJeune
2022-08-29 21:53:15 -04:00
parent c05a297327
commit 08ba3f1b08
3 changed files with 146 additions and 50 deletions

View File

@@ -272,10 +272,12 @@ fun MinLinesText(
) )
} }
class ChipInfo(val text: String, val enabled: Boolean = true)
sealed class ChipStyle(val mainAxisSpacing: Dp, val crossAxisSpacing: Dp) { sealed class ChipStyle(val mainAxisSpacing: Dp, val crossAxisSpacing: Dp) {
object Boxy: ChipStyle(8.dp, 4.dp) object Boxy: ChipStyle(8.dp, 4.dp)
object Rounded: ChipStyle(4.dp, 4.dp) object Rounded: ChipStyle(4.dp, 4.dp)
class Mixed(val predicate: (String) -> ChipStyle): ChipStyle(8.dp, 4.dp) class Mixed(val predicate: (ChipInfo) -> ChipStyle): ChipStyle(8.dp, 4.dp)
} }
@Composable @Composable
@@ -283,7 +285,8 @@ fun BoxyChip(
text: String, text: String,
style: TextStyle = MaterialTheme.typography.bodySmall, style: TextStyle = MaterialTheme.typography.bodySmall,
isSelected: Boolean = true, isSelected: Boolean = true,
onSelectionChanged: (String) -> Unit = {} onSelectionChanged: (String) -> Unit = {},
enabled: Boolean = true
) { ) {
Surface( Surface(
// modifier = Modifier.padding(4.dp), // modifier = Modifier.padding(4.dp),
@@ -297,7 +300,8 @@ fun BoxyChip(
value = isSelected, value = isSelected,
onValueChange = { onValueChange = {
onSelectionChanged(text) onSelectionChanged(text)
} },
enabled = enabled
) )
) { ) {
Text( Text(
@@ -315,7 +319,8 @@ fun RoundedChip(
text: String, text: String,
style: TextStyle = MaterialTheme.typography.bodySmall, style: TextStyle = MaterialTheme.typography.bodySmall,
isSelected: Boolean = false, isSelected: Boolean = false,
onSelectionChanged: (String) -> Unit = {} onSelectionChanged: (String) -> Unit = {},
enabled: Boolean = true
) { ) {
val borderColor = if (isSelected) MaterialTheme.colorScheme.inverseSurface else MaterialTheme.colorScheme.onSurfaceVariant val borderColor = if (isSelected) MaterialTheme.colorScheme.inverseSurface else MaterialTheme.colorScheme.onSurfaceVariant
val radius = style.fontSize.value.dp * 2 val radius = style.fontSize.value.dp * 2
@@ -330,7 +335,8 @@ fun RoundedChip(
value = isSelected, value = isSelected,
onValueChange = { onValueChange = {
onSelectionChanged(text) onSelectionChanged(text)
} },
enabled = enabled
) )
.padding(8.dp) .padding(8.dp)
) { ) {
@@ -346,24 +352,26 @@ fun RoundedChip(
@Composable @Composable
fun ChipGroup( fun ChipGroup(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
chips: List<String> = emptyList(), chips: List<ChipInfo> = emptyList(),
onSelectedChanged: (String) -> Unit = {}, onSelectedChanged: (String) -> Unit = {},
chipStyle: ChipStyle = ChipStyle.Boxy chipStyle: ChipStyle = ChipStyle.Boxy
) { ) {
@Composable @Composable
fun DrawChip(chipStyle: ChipStyle, chip: String) { fun DrawChip(chipStyle: ChipStyle, chip: ChipInfo) {
when (chipStyle) { when (chipStyle) {
ChipStyle.Boxy -> { ChipStyle.Boxy -> {
BoxyChip( BoxyChip(
text = chip, text = chip.text,
onSelectionChanged = onSelectedChanged onSelectionChanged = onSelectedChanged,
enabled = chip.enabled
) )
} }
ChipStyle.Rounded -> { ChipStyle.Rounded -> {
RoundedChip( RoundedChip(
text = chip, text = chip.text,
onSelectionChanged = onSelectedChanged onSelectionChanged = onSelectedChanged,
enabled = chip.enabled
) )
} }
is ChipStyle.Mixed -> { is ChipStyle.Mixed -> {

View File

@@ -116,6 +116,60 @@ fun DetailHeader(
} }
} }
@Composable
fun DetailHeader2(
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 (
backdropImage, posterImage, 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, margin = 16.dp)
},
url = posterUrl,
contentDescription = posterContentDescription,
elevation = 20.dp
)
rating?.let {
RatingView(
modifier = Modifier
.constrainAs(ratingsView) {
bottom.linkTo(parent.bottom)
start.linkTo(posterImage.end, margin = 20.dp)
},
progress = rating
)
}
}
}
@Composable @Composable
private fun Backdrop(modifier: Modifier, imageUrl: String?, contentDescription: String? = null) { private fun Backdrop(modifier: Modifier, imageUrl: String?, contentDescription: String? = null) {
// val images = remember { mutableStateOf<ImageCollection?>(null) } // val images = remember { mutableStateOf<ImageCollection?>(null) }
@@ -151,7 +205,7 @@ private fun TitleText(modifier: Modifier, title: String) {
} }
@Composable @Composable
private fun RatingView( fun RatingView(
progress: Float, progress: Float,
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {

View File

@@ -9,6 +9,7 @@ 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.CircleShape
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.*
@@ -18,6 +19,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
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.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
@@ -47,13 +49,14 @@ import kotlinx.coroutines.*
import org.json.JSONObject import org.json.JSONObject
import java.text.DecimalFormat import java.text.DecimalFormat
@OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun MediaDetailView( fun MediaDetailView(
appNavController: NavController, appNavController: NavController,
itemId: Int?, itemId: Int?,
type: MediaViewType type: MediaViewType
) { ) {
val service = when(type) { val service = when (type) {
MediaViewType.MOVIE -> MoviesService() MediaViewType.MOVIE -> MoviesService()
MediaViewType.TV -> TvService() MediaViewType.TV -> TvService()
else -> throw IllegalArgumentException("Media type given: ${type}, \n expected one of MediaViewType.MOVIE, MediaViewType.TV") // shouldn't happen else -> throw IllegalArgumentException("Media type given: ${type}, \n expected one of MediaViewType.MOVIE, MediaViewType.TV") // shouldn't happen
@@ -66,48 +69,79 @@ fun MediaDetailView(
} }
} }
Column( val decayAnimationSpec = rememberSplineBasedDecay<Float>()
modifier = Modifier val topAppBarScrollState = rememberTopAppBarScrollState()
.background(color = MaterialTheme.colorScheme.background) val scrollBehavior = remember(decayAnimationSpec) {
.verticalScroll(rememberScrollState()) TopAppBarDefaults.exitUntilCollapsedScrollBehavior(decayAnimationSpec, topAppBarScrollState)
.fillMaxSize() }
.padding(bottom = 16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
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 }
)
Column( Scaffold(
modifier = Modifier.padding(horizontal = 16.dp), modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
verticalArrangement = Arrangement.spacedBy(16.dp), topBar = {
// horizontalAlignment = Alignment.CenterHorizontally SmallTopAppBar(
) { scrollBehavior = scrollBehavior,
if (type == MediaViewType.MOVIE) { colors = TopAppBarDefaults
MiscMovieDetails(mediaItem = mediaItem, service as MoviesService) .largeTopAppBarColors(
} else { scrolledContainerColor = MaterialTheme.colorScheme.background,
MiscTvDetails(mediaItem = mediaItem, service as TvService) titleContentColor = MaterialTheme.colorScheme.primary
} ),
title = { Text(text = mediaItem.value?.title ?: "") },
navigationIcon = {
IconButton(
onClick = { appNavController.popBackStack() }
) {
Icon(
imageVector = Icons.Filled.ArrowBack,
contentDescription = stringResource(id = R.string.content_description_back_button),
tint = MaterialTheme.colorScheme.primary
)
}
}
)
}
) { innerPadding ->
Box(modifier = Modifier.padding(innerPadding)) {
Column(
modifier = Modifier
.background(color = MaterialTheme.colorScheme.background)
.verticalScroll(state = rememberScrollState())
.padding(bottom = 16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
DetailHeader2(
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 }
)
ActionsView(itemId = itemId, type = type, service = service) Column(
modifier = Modifier.padding(horizontal = 16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
if (type == MediaViewType.MOVIE) {
MiscMovieDetails(mediaItem = mediaItem, service as MoviesService)
} else {
MiscTvDetails(mediaItem = mediaItem, service as TvService)
}
OverviewCard(itemId = itemId, mediaItem = mediaItem, service = service) ActionsView(itemId = itemId, type = type, service = service)
CastCard(itemId = itemId, service = service, appNavController = appNavController) OverviewCard(itemId = itemId, mediaItem = mediaItem, service = service)
SimilarContentCard(itemId = itemId, service = service, mediaType = type, appNavController = appNavController) CastCard(itemId = itemId, service = service, appNavController = appNavController)
VideosCard(itemId = itemId, service = service) SimilarContentCard(itemId = itemId, service = service, mediaType = type, appNavController = appNavController)
ReviewsCard(itemId = itemId, service = service) VideosCard(itemId = itemId, service = service)
}
} ReviewsCard(itemId = itemId, service = service)
}
}
}
}
} }
@Composable @Composable
@@ -184,7 +218,7 @@ private fun MiscDetails(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.wrapContentHeight(), .wrapContentHeight(),
chips = genres.map { it.name } chips = genres.map { ChipInfo(it.name, false) }
) )
} }
} }
@@ -618,7 +652,7 @@ private fun OverviewCard(itemId: Int?, mediaItem: MutableState<DetailedItem?>, s
keywordResponse.value?.keywords?.let { keywords -> keywordResponse.value?.keywords?.let { keywords ->
val names = keywords.map { it.name } val names = keywords.map { ChipInfo(it.name, false) }
ChipGroup( ChipGroup(
chips = names, chips = names,
chipStyle = ChipStyle.Rounded, chipStyle = ChipStyle.Rounded,
@@ -677,7 +711,7 @@ private fun CastCrewCard(appNavController: NavController, person: Person) {
imageUrl = TmdbUtils.getFullPersonImagePath(person), imageUrl = TmdbUtils.getFullPersonImagePath(person),
noDataImage = R.drawable.no_person_photo, noDataImage = R.drawable.no_person_photo,
titleTextColor = MaterialTheme.colorScheme.onPrimary, titleTextColor = MaterialTheme.colorScheme.onPrimary,
subtitleTextColor = Color.Unspecified, subtitleTextColor = MaterialTheme.colorScheme.onSecondary,
onItemClicked = { onItemClicked = {
appNavController.navigate( appNavController.navigate(
"${MainNavItem.DetailView.route}/${MediaViewType.PERSON}/${person.id}" "${MainNavItem.DetailView.route}/${MediaViewType.PERSON}/${person.id}"