mirror of
https://github.com/owenlejeune/TVTime.git
synced 2025-11-18 01:30:54 -05:00
refactor media detail view
This commit is contained in:
@@ -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 -> {
|
||||||
|
|||||||
@@ -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
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -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,15 +69,46 @@ fun MediaDetailView(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val decayAnimationSpec = rememberSplineBasedDecay<Float>()
|
||||||
|
val topAppBarScrollState = rememberTopAppBarScrollState()
|
||||||
|
val scrollBehavior = remember(decayAnimationSpec) {
|
||||||
|
TopAppBarDefaults.exitUntilCollapsedScrollBehavior(decayAnimationSpec, topAppBarScrollState)
|
||||||
|
}
|
||||||
|
|
||||||
|
Scaffold(
|
||||||
|
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
|
||||||
|
topBar = {
|
||||||
|
SmallTopAppBar(
|
||||||
|
scrollBehavior = scrollBehavior,
|
||||||
|
colors = TopAppBarDefaults
|
||||||
|
.largeTopAppBarColors(
|
||||||
|
scrolledContainerColor = MaterialTheme.colorScheme.background,
|
||||||
|
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(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.background(color = MaterialTheme.colorScheme.background)
|
.background(color = MaterialTheme.colorScheme.background)
|
||||||
.verticalScroll(rememberScrollState())
|
.verticalScroll(state = rememberScrollState())
|
||||||
.fillMaxSize()
|
|
||||||
.padding(bottom = 16.dp),
|
.padding(bottom = 16.dp),
|
||||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||||
) {
|
) {
|
||||||
DetailHeader(
|
DetailHeader2(
|
||||||
appNavController = appNavController,
|
appNavController = appNavController,
|
||||||
title = mediaItem.value?.title ?: "",
|
title = mediaItem.value?.title ?: "",
|
||||||
posterUrl = TmdbUtils.getFullPosterPath(mediaItem.value?.posterPath),
|
posterUrl = TmdbUtils.getFullPosterPath(mediaItem.value?.posterPath),
|
||||||
@@ -85,8 +119,7 @@ fun MediaDetailView(
|
|||||||
|
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier.padding(horizontal = 16.dp),
|
modifier = Modifier.padding(horizontal = 16.dp),
|
||||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||||
// horizontalAlignment = Alignment.CenterHorizontally
|
|
||||||
) {
|
) {
|
||||||
if (type == MediaViewType.MOVIE) {
|
if (type == MediaViewType.MOVIE) {
|
||||||
MiscMovieDetails(mediaItem = mediaItem, service as MoviesService)
|
MiscMovieDetails(mediaItem = mediaItem, service as MoviesService)
|
||||||
@@ -106,7 +139,8 @@ fun MediaDetailView(
|
|||||||
|
|
||||||
ReviewsCard(itemId = itemId, service = service)
|
ReviewsCard(itemId = itemId, service = service)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -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}"
|
||||||
|
|||||||
Reference in New Issue
Block a user