mirror of
https://github.com/owenlejeune/TVTime.git
synced 2025-11-08 04:32:43 -05:00
begin skeleton loading imeplementation
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
package com.owenlejeune.tvtime.api
|
||||
|
||||
import androidx.compose.runtime.MutableState
|
||||
import kotlinx.coroutines.delay
|
||||
import retrofit2.Response
|
||||
|
||||
infix fun <T> Response<T>.storedIn(body: (T) -> Unit) {
|
||||
|
||||
@@ -5,4 +5,17 @@ import com.google.gson.annotations.SerializedName
|
||||
data class Genre(
|
||||
@SerializedName("id") val id: Int,
|
||||
@SerializedName("name") val name: String
|
||||
)
|
||||
) {
|
||||
|
||||
companion object {
|
||||
val placeholderData: List<Genre> by lazy {
|
||||
listOf<Genre>().toMutableList().apply {
|
||||
for (i in 0 until 3) {
|
||||
add(Genre(i, " "))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -8,4 +8,6 @@ fun Any.coroutineTask(runnable: suspend () -> Unit) {
|
||||
CoroutineScope(Dispatchers.IO).launch { runnable() }
|
||||
}
|
||||
|
||||
fun <T> anyOf(vararg items: T, predicate: (T) -> Boolean): Boolean = items.any(predicate)
|
||||
fun <T> anyOf(vararg items: T, predicate: (T) -> Boolean): Boolean = items.any(predicate)
|
||||
|
||||
fun <T: Any> T.isIn(vararg items: T): Boolean = items.any { it == this }
|
||||
@@ -17,7 +17,10 @@ import androidx.compose.ui.graphics.RectangleShape
|
||||
import androidx.compose.ui.graphics.Shape
|
||||
import androidx.compose.ui.graphics.TileMode
|
||||
|
||||
fun Modifier.shimmerBackground(shape: Shape = RectangleShape): Modifier = composed {
|
||||
fun Modifier.shimmerBackground(
|
||||
shape: Shape = RectangleShape,
|
||||
tint: Color = Color.LightGray
|
||||
): Modifier = composed {
|
||||
val transition = rememberInfiniteTransition()
|
||||
val translateAnimation by transition.animateFloat(
|
||||
initialValue = 0f,
|
||||
@@ -28,8 +31,8 @@ fun Modifier.shimmerBackground(shape: Shape = RectangleShape): Modifier = compos
|
||||
),
|
||||
)
|
||||
val shimmerColors = listOf(
|
||||
Color.LightGray.copy(alpha = 0.9f),
|
||||
Color.LightGray.copy(alpha = 0.4f),
|
||||
tint.copy(alpha = 0.9f),
|
||||
tint.copy(alpha = 0.4f),
|
||||
)
|
||||
val brush = Brush.linearGradient(
|
||||
colors = shimmerColors,
|
||||
|
||||
@@ -29,10 +29,14 @@ import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.owenlejeune.tvtime.api.LoadingState
|
||||
import com.owenlejeune.tvtime.extensions.isIn
|
||||
import com.owenlejeune.tvtime.extensions.shimmerBackground
|
||||
import com.owenlejeune.tvtime.ui.theme.FavoriteSelected
|
||||
import com.owenlejeune.tvtime.ui.theme.RatingSelected
|
||||
import com.owenlejeune.tvtime.ui.theme.WatchlistSelected
|
||||
import com.owenlejeune.tvtime.ui.viewmodel.AccountViewModel
|
||||
import com.owenlejeune.tvtime.ui.viewmodel.ApplicationViewModel
|
||||
import com.owenlejeune.tvtime.ui.viewmodel.MainViewModel
|
||||
import com.owenlejeune.tvtime.utils.types.MediaViewType
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@@ -6,12 +6,15 @@ import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
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.painter.Painter
|
||||
@@ -23,6 +26,7 @@ import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.IntSize
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import coil.compose.AsyncImage
|
||||
import coil.compose.rememberAsyncImagePainter
|
||||
import coil.request.CachePolicy
|
||||
@@ -32,9 +36,14 @@ import com.google.accompanist.pager.HorizontalPager
|
||||
import com.google.accompanist.pager.PagerState
|
||||
import com.google.accompanist.pager.rememberPagerState
|
||||
import com.owenlejeune.tvtime.R
|
||||
import com.owenlejeune.tvtime.api.LoadingState
|
||||
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.ExternalIds
|
||||
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.ImageCollection
|
||||
import com.owenlejeune.tvtime.extensions.shimmerBackground
|
||||
import com.owenlejeune.tvtime.extensions.toDp
|
||||
import com.owenlejeune.tvtime.ui.viewmodel.MainViewModel
|
||||
import com.owenlejeune.tvtime.utils.TmdbUtils
|
||||
import com.owenlejeune.tvtime.utils.types.MediaViewType
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@@ -54,7 +63,9 @@ fun DetailHeader(
|
||||
) {
|
||||
Box(
|
||||
modifier = modifier.then(
|
||||
Modifier.fillMaxWidth().wrapContentHeight()
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.wrapContentHeight()
|
||||
)
|
||||
) {
|
||||
if (imageCollection != null) {
|
||||
@@ -119,7 +130,8 @@ private fun BackdropContainer(
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.matchParentSize()
|
||||
.width(sizeImage.value.width.toDp())
|
||||
.height(sizeImage.value.height.toDp())
|
||||
.background(gradient)
|
||||
)
|
||||
}
|
||||
@@ -214,10 +226,35 @@ fun BackdropGallery(
|
||||
|
||||
@Composable
|
||||
fun ExternalIdsArea(
|
||||
externalIds: ExternalIds,
|
||||
type: MediaViewType,
|
||||
itemId: Int,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
if (externalIds.hasExternalIds()) {
|
||||
val mainViewModel = viewModel<MainViewModel>()
|
||||
val loadingState = remember { mainViewModel.produceExternalIdsLoadingStateFor(type) }
|
||||
|
||||
val externalIdsMap = remember { mainViewModel.produceExternalIdsFor(type) }
|
||||
val externalIds = externalIdsMap[itemId]
|
||||
|
||||
if (loadingState.value == LoadingState.LOADING || loadingState.value == LoadingState.REFRESHING) {
|
||||
Row(
|
||||
modifier = modifier,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
for (i in 0 until 4) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.clip(RoundedCornerShape(5.dp))
|
||||
.size(28.dp)
|
||||
.shimmerBackground(
|
||||
RoundedCornerShape(5.dp),
|
||||
tint = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
} else if (externalIds?.hasExternalIds() == true) {
|
||||
Row(
|
||||
modifier = modifier,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
@@ -264,4 +301,32 @@ private fun ExternalIdLogo(
|
||||
modifier = Modifier.size(28.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun PlaceholderDetailHeader() {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.wrapContentHeight()
|
||||
.aspectRatio(1.778f)
|
||||
.shimmerBackground()
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.align(Alignment.BottomStart)
|
||||
.padding(start = 16.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(20.dp),
|
||||
verticalAlignment = Alignment.Bottom
|
||||
) {
|
||||
PlaceholderPosterItem()
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.clip(CircleShape)
|
||||
.size(60.dp)
|
||||
.shimmerBackground(tint = MaterialTheme.colorScheme.secondary)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -172,14 +172,15 @@ fun PosterItem(
|
||||
var sizeImage by remember { mutableStateOf(IntSize.Zero) }
|
||||
Card(
|
||||
elevation = CardDefaults.elevatedCardElevation(defaultElevation = elevation),
|
||||
modifier = modifier
|
||||
modifier = modifier.then(Modifier
|
||||
.width(width = width)
|
||||
.height(height = height)
|
||||
.wrapContentHeight()
|
||||
.clickable(
|
||||
enabled = enabled,
|
||||
onClick = onClick
|
||||
),
|
||||
)
|
||||
),
|
||||
shape = RoundedCornerShape(5.dp),
|
||||
colors = CardDefaults.cardColors(containerColor = Color.Transparent)
|
||||
) {
|
||||
@@ -258,4 +259,21 @@ fun PosterItem(
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun PlaceholderPosterItem(
|
||||
modifier: Modifier = Modifier,
|
||||
width: Dp = POSTER_WIDTH,
|
||||
height: Dp = POSTER_HEIGHT,
|
||||
tint: Color = Color.LightGray
|
||||
) {
|
||||
Box(
|
||||
modifier = modifier.then(Modifier
|
||||
.width(width)
|
||||
.height(height)
|
||||
.clip(RoundedCornerShape(5.dp))
|
||||
.shimmerBackground(RoundedCornerShape(5.dp), tint)
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -68,6 +68,7 @@ import coil.request.ImageRequest
|
||||
import com.google.accompanist.flowlayout.FlowRow
|
||||
import com.owenlejeune.tvtime.R
|
||||
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.AuthorDetails
|
||||
import com.owenlejeune.tvtime.extensions.shimmerBackground
|
||||
import com.owenlejeune.tvtime.extensions.toDp
|
||||
import com.owenlejeune.tvtime.extensions.unlessEmpty
|
||||
import com.owenlejeune.tvtime.preferences.AppPreferences
|
||||
@@ -285,7 +286,17 @@ class ChipInfo(
|
||||
val text: String,
|
||||
val enabled: Boolean = true,
|
||||
val id: Int? = null
|
||||
)
|
||||
) {
|
||||
companion object {
|
||||
fun generatePlaceholders(size: Int): List<ChipInfo> {
|
||||
return emptyList<ChipInfo>().toMutableList().apply {
|
||||
for (i in 0 until size) {
|
||||
add(ChipInfo(""))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sealed class ChipStyle(val mainAxisSpacing: Dp, val crossAxisSpacing: Dp) {
|
||||
object Boxy: ChipStyle(8.dp, 4.dp)
|
||||
@@ -338,15 +349,17 @@ fun BoxyChip(
|
||||
style: TextStyle = MaterialTheme.typography.bodySmall,
|
||||
isSelected: Boolean = true,
|
||||
onSelectionChanged: (ChipInfo) -> Unit = {},
|
||||
colors: ChipColors = ChipDefaults.boxyChipColors()
|
||||
colors: ChipColors = ChipDefaults.boxyChipColors(),
|
||||
isLoading: Boolean = false
|
||||
) {
|
||||
val shimmerModifier = if (isLoading) Modifier.shimmerBackground(shape = RoundedCornerShape(5.dp)) else Modifier
|
||||
Surface(
|
||||
shadowElevation = 2.dp,
|
||||
shape = RoundedCornerShape(5.dp),
|
||||
color = if (isSelected) colors.selectedContainerColor() else colors.unselectedContainerColor()
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
modifier = shimmerModifier
|
||||
.toggleable(
|
||||
value = isSelected,
|
||||
onValueChange = {
|
||||
@@ -367,21 +380,25 @@ fun BoxyChip(
|
||||
|
||||
@Composable
|
||||
fun RoundedChip(
|
||||
modifier: Modifier = Modifier,
|
||||
chipInfo: ChipInfo,
|
||||
style: TextStyle = MaterialTheme.typography.bodySmall,
|
||||
isSelected: Boolean = false,
|
||||
onSelectionChanged: (ChipInfo) -> Unit = {},
|
||||
colors: ChipColors = ChipDefaults.roundedChipColors()
|
||||
colors: ChipColors = ChipDefaults.roundedChipColors(),
|
||||
isLoading: Boolean = false
|
||||
) {
|
||||
val borderColor = if (isSelected) colors.selectedContainerColor() else colors.unselectedContainerColor()
|
||||
val radius = style.fontSize.value.dp * 2
|
||||
Surface(
|
||||
modifier = modifier,
|
||||
border = BorderStroke(width = 1.dp, borderColor),
|
||||
shape = RoundedCornerShape(radius),
|
||||
color = MaterialTheme.colorScheme.surfaceVariant
|
||||
) {
|
||||
val shimmerModifier = if(isLoading) Modifier.shimmerBackground(RoundedCornerShape(radius)) else Modifier
|
||||
Row(
|
||||
modifier = Modifier
|
||||
modifier = shimmerModifier
|
||||
.toggleable(
|
||||
value = isSelected,
|
||||
onValueChange = {
|
||||
@@ -403,6 +420,7 @@ fun RoundedChip(
|
||||
@Composable
|
||||
fun ChipGroup(
|
||||
modifier: Modifier = Modifier,
|
||||
isLoading: Boolean = false,
|
||||
chips: List<ChipInfo> = emptyList(),
|
||||
onSelectedChanged: (ChipInfo) -> Unit = {},
|
||||
chipStyle: ChipStyle = ChipStyle.Boxy,
|
||||
@@ -411,24 +429,26 @@ fun ChipGroup(
|
||||
) {
|
||||
|
||||
@Composable
|
||||
fun DrawChip(chipStyle: ChipStyle, chip: ChipInfo) {
|
||||
fun DrawChip(chipStyle: ChipStyle, chip: ChipInfo, isLoading: Boolean) {
|
||||
when (chipStyle) {
|
||||
ChipStyle.Boxy -> {
|
||||
BoxyChip(
|
||||
chipInfo = chip,
|
||||
onSelectionChanged = onSelectedChanged,
|
||||
colors = boxyChipColors
|
||||
colors = boxyChipColors,
|
||||
isLoading = isLoading
|
||||
)
|
||||
}
|
||||
ChipStyle.Rounded -> {
|
||||
RoundedChip(
|
||||
chipInfo = chip,
|
||||
onSelectionChanged = onSelectedChanged,
|
||||
colors = roundedChipColors
|
||||
colors = roundedChipColors,
|
||||
isLoading = isLoading
|
||||
)
|
||||
}
|
||||
is ChipStyle.Mixed -> {
|
||||
DrawChip(chipStyle = chipStyle.predicate(chip), chip = chip)
|
||||
DrawChip(chipStyle = chipStyle.predicate(chip), chip = chip, isLoading = isLoading)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -439,7 +459,7 @@ fun ChipGroup(
|
||||
mainAxisSpacing = chipStyle.mainAxisSpacing
|
||||
) {
|
||||
chips.forEach { chip ->
|
||||
DrawChip(chipStyle = chipStyle, chip = chip)
|
||||
DrawChip(chipStyle = chipStyle, chip = chip, isLoading = isLoading)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,7 +47,6 @@ import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
@@ -73,6 +72,7 @@ import com.google.accompanist.pager.ExperimentalPagerApi
|
||||
import com.google.accompanist.pager.PagerState
|
||||
import com.google.accompanist.pager.rememberPagerState
|
||||
import com.owenlejeune.tvtime.R
|
||||
import com.owenlejeune.tvtime.api.LoadingState
|
||||
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.DetailedItem
|
||||
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.DetailedMovie
|
||||
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.DetailedTv
|
||||
@@ -89,8 +89,10 @@ import com.owenlejeune.tvtime.extensions.DateFormat
|
||||
import com.owenlejeune.tvtime.extensions.WindowSizeClass
|
||||
import com.owenlejeune.tvtime.extensions.format
|
||||
import com.owenlejeune.tvtime.extensions.getCalendarYear
|
||||
import com.owenlejeune.tvtime.extensions.isIn
|
||||
import com.owenlejeune.tvtime.extensions.lazyPagingItems
|
||||
import com.owenlejeune.tvtime.extensions.listItems
|
||||
import com.owenlejeune.tvtime.extensions.shimmerBackground
|
||||
import com.owenlejeune.tvtime.preferences.AppPreferences
|
||||
import com.owenlejeune.tvtime.ui.components.ActionsView
|
||||
import com.owenlejeune.tvtime.ui.components.AvatarImage
|
||||
@@ -109,6 +111,8 @@ import com.owenlejeune.tvtime.ui.components.HtmlText
|
||||
import com.owenlejeune.tvtime.ui.components.ImageGalleryOverlay
|
||||
import com.owenlejeune.tvtime.ui.components.ListContentCard
|
||||
import com.owenlejeune.tvtime.ui.components.PillSegmentedControl
|
||||
import com.owenlejeune.tvtime.ui.components.PlaceholderDetailHeader
|
||||
import com.owenlejeune.tvtime.ui.components.PlaceholderPosterItem
|
||||
import com.owenlejeune.tvtime.ui.components.PosterItem
|
||||
import com.owenlejeune.tvtime.ui.components.RoundedChip
|
||||
import com.owenlejeune.tvtime.ui.components.RoundedTextField
|
||||
@@ -121,31 +125,36 @@ import com.owenlejeune.tvtime.ui.viewmodel.SpecialFeaturesViewModel
|
||||
import com.owenlejeune.tvtime.utils.SessionManager
|
||||
import com.owenlejeune.tvtime.utils.TmdbUtils
|
||||
import com.owenlejeune.tvtime.utils.types.MediaViewType
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import org.koin.java.KoinJavaComponent.get
|
||||
import org.koin.java.KoinJavaComponent
|
||||
|
||||
private suspend fun fetchData(
|
||||
private fun fetchData(
|
||||
mainViewModel: MainViewModel,
|
||||
itemId: Int,
|
||||
type: MediaViewType,
|
||||
force: Boolean = false
|
||||
) {
|
||||
mainViewModel.getById(itemId, type, force)
|
||||
mainViewModel.getImages(itemId, type, force)
|
||||
mainViewModel.getExternalIds(itemId, type, force)
|
||||
mainViewModel.getKeywords(itemId, type, force)
|
||||
mainViewModel.getCastAndCrew(itemId, type, force)
|
||||
mainViewModel.getSimilar(itemId, type)
|
||||
mainViewModel.getVideos(itemId, type, force)
|
||||
mainViewModel.getWatchProviders(itemId, type, force)
|
||||
mainViewModel.getReviews(itemId, type, force)
|
||||
val scope = CoroutineScope(Dispatchers.IO)
|
||||
|
||||
scope.launch { mainViewModel.getById(itemId, type, force) }
|
||||
scope.launch { mainViewModel.getImages(itemId, type, force) }
|
||||
scope.launch { mainViewModel.getExternalIds(itemId, type, force) }
|
||||
scope.launch { mainViewModel.getKeywords(itemId, type, force) }
|
||||
scope.launch { mainViewModel.getCastAndCrew(itemId, type, force) }
|
||||
scope.launch { mainViewModel.getSimilar(itemId, type) }
|
||||
scope.launch { mainViewModel.getVideos(itemId, type, force) }
|
||||
scope.launch { mainViewModel.getWatchProviders(itemId, type, force) }
|
||||
scope.launch { mainViewModel.getReviews(itemId, type, force) }
|
||||
scope.launch { mainViewModel.getAccountStates(itemId, type) }
|
||||
|
||||
when (type) {
|
||||
MediaViewType.MOVIE -> {
|
||||
mainViewModel.getReleaseDates(itemId, force)
|
||||
scope.launch { mainViewModel.getReleaseDates(itemId, force) }
|
||||
}
|
||||
MediaViewType.TV -> {
|
||||
mainViewModel.getContentRatings(itemId, force)
|
||||
scope.launch { mainViewModel.getContentRatings(itemId, force) }
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
@@ -159,8 +168,6 @@ fun MediaDetailScreen(
|
||||
type: MediaViewType,
|
||||
windowSize: WindowSizeClass
|
||||
) {
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
val mainViewModel = viewModel<MainViewModel>()
|
||||
val applicationViewModel = viewModel<ApplicationViewModel>()
|
||||
|
||||
@@ -193,12 +200,11 @@ fun MediaDetailScreen(
|
||||
|
||||
val isRefreshing = remember { mutableStateOf(false) }
|
||||
mainViewModel.monitorDetailsLoadingRefreshing(refreshing = isRefreshing)
|
||||
|
||||
val pullRefreshState = rememberPullRefreshState(
|
||||
refreshing = isRefreshing.value,
|
||||
onRefresh = {
|
||||
scope.launch {
|
||||
fetchData(mainViewModel, itemId, type, true)
|
||||
}
|
||||
fetchData(mainViewModel, itemId, type, true)
|
||||
}
|
||||
)
|
||||
|
||||
@@ -232,7 +238,7 @@ fun MediaDetailScreen(
|
||||
windowSize = windowSize,
|
||||
showImageGallery = showGalleryOverlay,
|
||||
pagerState = pagerState,
|
||||
mainViewModel = mainViewModel
|
||||
mainViewModel = mainViewModel
|
||||
)
|
||||
PullRefreshIndicator(
|
||||
refreshing = isRefreshing.value,
|
||||
@@ -254,9 +260,9 @@ fun MediaDetailScreen(
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalPagerApi::class, ExperimentalMaterialApi::class)
|
||||
@OptIn(ExperimentalPagerApi::class)
|
||||
@Composable
|
||||
private fun MediaViewContent(
|
||||
fun MediaViewContent(
|
||||
appNavController: NavController,
|
||||
mainViewModel: MainViewModel,
|
||||
itemId: Int,
|
||||
@@ -266,7 +272,7 @@ private fun MediaViewContent(
|
||||
windowSize: WindowSizeClass,
|
||||
showImageGallery: MutableState<Boolean>,
|
||||
pagerState: PagerState,
|
||||
preferences: AppPreferences = get(AppPreferences::class.java)
|
||||
preferences: AppPreferences = KoinJavaComponent.get(AppPreferences::class.java)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
@@ -280,15 +286,22 @@ private fun MediaViewContent(
|
||||
.verticalScroll(state = rememberScrollState()),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
DetailHeader(
|
||||
posterUrl = TmdbUtils.getFullPosterPath(mediaItem?.posterPath),
|
||||
posterContentDescription = mediaItem?.title,
|
||||
backdropUrl = TmdbUtils.getFullBackdropPath(mediaItem?.backdropPath),
|
||||
rating = mediaItem?.voteAverage?.let { it / 10 },
|
||||
imageCollection = images,
|
||||
showGalleryOverlay = showImageGallery,
|
||||
pagerState = pagerState
|
||||
)
|
||||
val detailsLoadingState = remember { mainViewModel.produceDetailsLoadingStateFor(type) }
|
||||
val isLoading = detailsLoadingState.value.isIn(LoadingState.LOADING, LoadingState.REFRESHING)
|
||||
|
||||
if (isLoading) {
|
||||
PlaceholderDetailHeader()
|
||||
} else {
|
||||
DetailHeader(
|
||||
posterUrl = TmdbUtils.getFullPosterPath(mediaItem?.posterPath),
|
||||
posterContentDescription = mediaItem?.title,
|
||||
backdropUrl = TmdbUtils.getFullBackdropPath(mediaItem?.backdropPath),
|
||||
rating = mediaItem?.voteAverage?.let { it / 10 },
|
||||
imageCollection = images,
|
||||
showGalleryOverlay = showImageGallery,
|
||||
pagerState = pagerState
|
||||
)
|
||||
}
|
||||
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||
@@ -300,13 +313,11 @@ private fun MediaViewContent(
|
||||
itemId = itemId
|
||||
)
|
||||
|
||||
val externalIdsMap = remember { mainViewModel.produceExternalIdsFor(type) }
|
||||
externalIdsMap[itemId]?.let {
|
||||
ExternalIdsArea(
|
||||
externalIds = it,
|
||||
modifier = Modifier.padding(start = 20.dp)
|
||||
)
|
||||
}
|
||||
ExternalIdsArea(
|
||||
modifier = Modifier.padding(start = 20.dp),
|
||||
type = type,
|
||||
itemId = itemId
|
||||
)
|
||||
|
||||
val currentSession = remember { SessionManager.currentSession }
|
||||
currentSession.value?.let {
|
||||
@@ -409,7 +420,8 @@ private fun MiscTvDetails(
|
||||
year = TmdbUtils.getSeriesRun(series),
|
||||
runtime = TmdbUtils.convertRuntimeToHoursMinutes(series),
|
||||
genres = series.genres,
|
||||
contentRating = contentRating
|
||||
contentRating = contentRating,
|
||||
type = MediaViewType.TV
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -420,23 +432,22 @@ private fun MiscMovieDetails(
|
||||
mediaItem: DetailedItem?,
|
||||
mainViewModel: MainViewModel
|
||||
) {
|
||||
mediaItem?.let { mi ->
|
||||
val movie = mi as DetailedMovie
|
||||
val movie = mediaItem as? DetailedMovie
|
||||
|
||||
val contentRatingsMap = remember { mainViewModel.movieReleaseDates }
|
||||
val contentRating = TmdbUtils.getMovieRating(contentRatingsMap[itemId])
|
||||
val contentRatingsMap = remember { mainViewModel.movieReleaseDates }
|
||||
val contentRating = TmdbUtils.getMovieRating(contentRatingsMap[itemId])
|
||||
|
||||
MiscDetails(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.wrapContentHeight()
|
||||
.padding(horizontal = 16.dp),
|
||||
year = movie.releaseDate?.getCalendarYear()?.toString() ?: "",
|
||||
runtime = TmdbUtils.convertRuntimeToHoursMinutes(movie),
|
||||
genres = movie.genres,
|
||||
contentRating = contentRating
|
||||
)
|
||||
}
|
||||
MiscDetails(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.wrapContentHeight()
|
||||
.padding(horizontal = 16.dp),
|
||||
year = movie?.releaseDate?.getCalendarYear()?.toString() ?: "",
|
||||
runtime = TmdbUtils.convertRuntimeToHoursMinutes(movie),
|
||||
genres = movie?.genres ?: emptyList(),
|
||||
contentRating = contentRating,
|
||||
type = MediaViewType.MOVIE
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
@@ -445,8 +456,19 @@ private fun MiscDetails(
|
||||
year: String,
|
||||
runtime: String,
|
||||
genres: List<Genre>,
|
||||
contentRating: String
|
||||
contentRating: String,
|
||||
type: MediaViewType
|
||||
) {
|
||||
val mainViewModel = viewModel<MainViewModel>()
|
||||
val detailsLoadingState = remember { mainViewModel.produceDetailsLoadingStateFor(type) }
|
||||
val isLoading = detailsLoadingState.value.isIn(LoadingState.LOADING, LoadingState.REFRESHING)
|
||||
val shimmerModifier = if (isLoading) {
|
||||
Modifier
|
||||
.shimmerBackground(RoundedCornerShape(5.dp))
|
||||
.width(40.dp)
|
||||
} else {
|
||||
Modifier
|
||||
}
|
||||
Column(
|
||||
modifier = modifier,
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
@@ -454,28 +476,35 @@ private fun MiscDetails(
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.wrapContentHeight()
|
||||
.wrapContentHeight(),
|
||||
horizontalArrangement = Arrangement.spacedBy(if (isLoading) 6.dp else 0.dp)
|
||||
) {
|
||||
Text(text = year, color = MaterialTheme.colorScheme.onBackground)
|
||||
if (runtime != "0m") {
|
||||
Text(
|
||||
text = year.takeUnless { isLoading } ?: "",
|
||||
color = MaterialTheme.colorScheme.onBackground,
|
||||
modifier = shimmerModifier
|
||||
)
|
||||
if (runtime != "0m" || isLoading) {
|
||||
Text(
|
||||
text = runtime,
|
||||
text = runtime.takeUnless { isLoading } ?: "",
|
||||
color = MaterialTheme.colorScheme.onBackground,
|
||||
modifier = Modifier.padding(start = 12.dp)
|
||||
modifier = shimmerModifier.padding(start = 12.dp)
|
||||
)
|
||||
}
|
||||
Text(
|
||||
text = contentRating,
|
||||
text = contentRating.takeUnless { isLoading } ?: "",
|
||||
color = MaterialTheme.colorScheme.onBackground,
|
||||
modifier = Modifier.padding(start = 12.dp)
|
||||
modifier = shimmerModifier.padding(start = 12.dp)
|
||||
)
|
||||
}
|
||||
|
||||
val chips = if (isLoading) Genre.placeholderData else genres
|
||||
ChipGroup(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.wrapContentHeight(),
|
||||
chips = genres.map { ChipInfo(it.name, false) }
|
||||
chips = chips.map { ChipInfo(it.name, false) },
|
||||
isLoading = isLoading
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -492,62 +521,127 @@ private fun OverviewCard(
|
||||
val keywordsMap = remember { mainViewModel.produceKeywordsFor(type) }
|
||||
val keywords = keywordsMap[itemId]
|
||||
|
||||
mediaItem?.let { mi ->
|
||||
if (!mi.tagline.isNullOrEmpty() || keywords?.isNotEmpty() == true || !mi.overview.isNullOrEmpty()) {
|
||||
ContentCard(
|
||||
modifier = modifier
|
||||
val detailsLoadingState = remember { mainViewModel.produceAccountStatesLoadingStateFor(type) }
|
||||
val isLoading = detailsLoadingState.value.isIn(LoadingState.LOADING, LoadingState.REFRESHING)
|
||||
|
||||
if (isLoading) {
|
||||
ContentCard(
|
||||
modifier = modifier
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.wrapContentHeight(),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
Column(
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 16.dp)
|
||||
.width(150.dp)
|
||||
.shimmerBackground(RoundedCornerShape(10.dp)),
|
||||
text = "",
|
||||
color = MaterialTheme.colorScheme.tertiary,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
fontStyle = FontStyle.Italic,
|
||||
)
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 16.dp)
|
||||
.fillMaxWidth()
|
||||
.wrapContentHeight(),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
.height(100.dp)
|
||||
.shimmerBackground(RoundedCornerShape(10.dp)),
|
||||
text = "",
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
|
||||
|
||||
Row(
|
||||
modifier = Modifier.horizontalScroll(rememberScrollState()),
|
||||
horizontalArrangement = Arrangement.spacedBy(ChipStyle.Rounded.mainAxisSpacing)
|
||||
) {
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
mi.tagline?.let { tagline ->
|
||||
if (tagline.isNotEmpty()) {
|
||||
Text(
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
text = tagline,
|
||||
color = MaterialTheme.colorScheme.tertiary
|
||||
,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
fontStyle = FontStyle.Italic,
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
ChipInfo.generatePlaceholders(4).forEach { keywordChipInfo ->
|
||||
RoundedChip(
|
||||
modifier = Modifier.width(60.dp),
|
||||
isLoading = true,
|
||||
chipInfo = keywordChipInfo,
|
||||
colors = ChipDefaults.roundedChipColors(
|
||||
unselectedContainerColor = MaterialTheme.colorScheme.primary,
|
||||
unselectedContentColor = MaterialTheme.colorScheme.primary
|
||||
),
|
||||
onSelectionChanged = {}
|
||||
)
|
||||
}
|
||||
Text(
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
text = mi.overview ?: "",
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
keywords?.let { keywords ->
|
||||
val keywordsChipInfo = keywords.map { ChipInfo(it.name, true, it.id) }
|
||||
Row(
|
||||
modifier = Modifier.horizontalScroll(rememberScrollState()),
|
||||
horizontalArrangement = Arrangement.spacedBy(ChipStyle.Rounded.mainAxisSpacing)
|
||||
) {
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
keywordsChipInfo.forEach { keywordChipInfo ->
|
||||
RoundedChip(
|
||||
chipInfo = keywordChipInfo,
|
||||
colors = ChipDefaults.roundedChipColors(
|
||||
unselectedContainerColor = MaterialTheme.colorScheme.primary,
|
||||
unselectedContentColor = MaterialTheme.colorScheme.primary
|
||||
),
|
||||
onSelectionChanged = { chip ->
|
||||
appNavController.navigate(AppNavItem.KeywordsView.withArgs(type, chip.text, chip.id!!))
|
||||
}
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
mediaItem?.let { mi ->
|
||||
if (!mi.tagline.isNullOrEmpty() || keywords?.isNotEmpty() == true || !mi.overview.isNullOrEmpty()) {
|
||||
ContentCard(
|
||||
modifier = modifier
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.wrapContentHeight(),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
mi.tagline?.let { tagline ->
|
||||
if (tagline.isNotEmpty()) {
|
||||
Text(
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
text = tagline,
|
||||
color = MaterialTheme.colorScheme.tertiary,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
fontStyle = FontStyle.Italic,
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
}
|
||||
}
|
||||
Text(
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
text = mi.overview ?: "",
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
keywords?.let { keywords ->
|
||||
val keywordsChipInfo = keywords.map { ChipInfo(it.name, true, it.id) }
|
||||
Row(
|
||||
modifier = Modifier.horizontalScroll(rememberScrollState()),
|
||||
horizontalArrangement = Arrangement.spacedBy(ChipStyle.Rounded.mainAxisSpacing)
|
||||
) {
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
keywordsChipInfo.forEach { keywordChipInfo ->
|
||||
RoundedChip(
|
||||
chipInfo = keywordChipInfo,
|
||||
colors = ChipDefaults.roundedChipColors(
|
||||
unselectedContainerColor = MaterialTheme.colorScheme.primary,
|
||||
unselectedContentColor = MaterialTheme.colorScheme.primary
|
||||
),
|
||||
onSelectionChanged = { chip ->
|
||||
appNavController.navigate(
|
||||
AppNavItem.KeywordsView.withArgs(
|
||||
type,
|
||||
chip.text,
|
||||
chip.id!!
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -702,6 +796,9 @@ private fun CastCard(
|
||||
val castMap = remember { mainViewModel.produceCastFor(type) }
|
||||
val cast = castMap[itemId]
|
||||
|
||||
val loadingState = remember { mainViewModel.produceDetailsLoadingStateFor(type) }
|
||||
val isLoading = loadingState.value.isIn(LoadingState.LOADING, LoadingState.REFRESHING)
|
||||
|
||||
ContentCard(
|
||||
modifier = modifier,
|
||||
title = stringResource(R.string.cast_label),
|
||||
@@ -718,9 +815,15 @@ private fun CastCard(
|
||||
item {
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
}
|
||||
items(cast?.size ?: 0) { i ->
|
||||
cast?.get(i)?.let {
|
||||
CastCrewCard(appNavController = appNavController, person = it)
|
||||
if (isLoading) {
|
||||
items(5) {
|
||||
PlaceholderPosterItem()
|
||||
}
|
||||
} else {
|
||||
items(cast?.size ?: 0) { i ->
|
||||
cast?.get(i)?.let {
|
||||
CastCrewCard(appNavController = appNavController, person = it)
|
||||
}
|
||||
}
|
||||
}
|
||||
item {
|
||||
@@ -728,27 +831,40 @@ private fun CastCard(
|
||||
}
|
||||
}
|
||||
|
||||
Text(
|
||||
text = stringResource(R.string.see_all_cast_and_crew),
|
||||
fontSize = 12.sp,
|
||||
color = MaterialTheme.colorScheme.inversePrimary,
|
||||
modifier = Modifier
|
||||
.padding(start = 12.dp, bottom = 12.dp)
|
||||
.clickable {
|
||||
appNavController.navigate(
|
||||
AppNavItem.CastCrewListView.withArgs(
|
||||
type,
|
||||
itemId
|
||||
if (isLoading) {
|
||||
Text(
|
||||
text = "",
|
||||
modifier = Modifier
|
||||
.padding(start = 12.dp, bottom = 12.dp)
|
||||
.width(80.dp)
|
||||
.shimmerBackground(RoundedCornerShape(10.dp))
|
||||
)
|
||||
} else {
|
||||
Text(
|
||||
text = stringResource(R.string.see_all_cast_and_crew),
|
||||
fontSize = 12.sp,
|
||||
color = MaterialTheme.colorScheme.inversePrimary,
|
||||
modifier = Modifier
|
||||
.padding(start = 12.dp, bottom = 12.dp)
|
||||
.clickable {
|
||||
appNavController.navigate(
|
||||
AppNavItem.CastCrewListView.withArgs(
|
||||
type,
|
||||
itemId
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun CastCrewCard(appNavController: NavController, person: Person) {
|
||||
private fun CastCrewCard(
|
||||
appNavController: NavController,
|
||||
person: Person
|
||||
) {
|
||||
TwoLineImageTextCard(
|
||||
title = person.name,
|
||||
modifier = Modifier
|
||||
@@ -952,7 +1068,7 @@ private fun VideoGroup(results: List<Video>, type: Video.Type, title: String) {
|
||||
|
||||
val posterWidth = 120.dp
|
||||
LazyRow(modifier = Modifier
|
||||
.padding(vertical = 8.dp),
|
||||
.padding(vertical = 8.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp)
|
||||
) {
|
||||
item {
|
||||
@@ -1193,124 +1309,124 @@ private fun ReviewsCard(
|
||||
val reviewsMap = remember { mainViewModel.produceReviewsFor(type) }
|
||||
val reviews = reviewsMap[itemId]
|
||||
|
||||
ListContentCard(
|
||||
modifier = modifier,
|
||||
header = {
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(9.dp)
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.reviews_title),
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
ListContentCard(
|
||||
modifier = modifier,
|
||||
header = {
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(9.dp)
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.reviews_title),
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
|
||||
if (SessionManager.currentSession.value?.isAuthorized == true) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(50.dp)
|
||||
.padding(bottom = 4.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
var reviewTextState by remember { mutableStateOf("") }
|
||||
if (SessionManager.currentSession.value?.isAuthorized == true) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(50.dp)
|
||||
.padding(bottom = 4.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
var reviewTextState by remember { mutableStateOf("") }
|
||||
|
||||
RoundedTextField(
|
||||
modifier = Modifier
|
||||
.height(40.dp)
|
||||
.align(Alignment.CenterVertically)
|
||||
.weight(1f),
|
||||
value = reviewTextState,
|
||||
onValueChange = { reviewTextState = it },
|
||||
placeHolder = stringResource(R.string.add_a_review_hint),
|
||||
backgroundColor = MaterialTheme.colorScheme.secondary,
|
||||
placeHolderTextColor = MaterialTheme.colorScheme.background,
|
||||
textColor = MaterialTheme.colorScheme.onSecondary
|
||||
)
|
||||
RoundedTextField(
|
||||
modifier = Modifier
|
||||
.height(40.dp)
|
||||
.align(Alignment.CenterVertically)
|
||||
.weight(1f),
|
||||
value = reviewTextState,
|
||||
onValueChange = { reviewTextState = it },
|
||||
placeHolder = stringResource(R.string.add_a_review_hint),
|
||||
backgroundColor = MaterialTheme.colorScheme.secondary,
|
||||
placeHolderTextColor = MaterialTheme.colorScheme.background,
|
||||
textColor = MaterialTheme.colorScheme.onSecondary
|
||||
)
|
||||
|
||||
val context = LocalContext.current
|
||||
CircleBackgroundColorImage(
|
||||
modifier = Modifier
|
||||
.align(Alignment.CenterVertically)
|
||||
.clickable(
|
||||
onClick = {
|
||||
Toast
|
||||
.makeText(context, "TODO", Toast.LENGTH_SHORT)
|
||||
.show()
|
||||
}
|
||||
),
|
||||
size = 40.dp,
|
||||
backgroundColor = MaterialTheme.colorScheme.tertiary,
|
||||
image = Icons.Filled.Send,
|
||||
colorFilter = ColorFilter.tint(color = MaterialTheme.colorScheme.surfaceVariant),
|
||||
contentDescription = ""
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
) {
|
||||
if (reviews?.isNotEmpty() == true) {
|
||||
reviews.reversed().forEachIndexed { index, review ->
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(end = 16.dp),
|
||||
verticalAlignment = Alignment.Top,
|
||||
horizontalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
AvatarImage(
|
||||
size = 50.dp,
|
||||
author = review.authorDetails
|
||||
)
|
||||
val context = LocalContext.current
|
||||
CircleBackgroundColorImage(
|
||||
modifier = Modifier
|
||||
.align(Alignment.CenterVertically)
|
||||
.clickable(
|
||||
onClick = {
|
||||
Toast
|
||||
.makeText(context, "TODO", Toast.LENGTH_SHORT)
|
||||
.show()
|
||||
}
|
||||
),
|
||||
size = 40.dp,
|
||||
backgroundColor = MaterialTheme.colorScheme.tertiary,
|
||||
image = Icons.Filled.Send,
|
||||
colorFilter = ColorFilter.tint(color = MaterialTheme.colorScheme.surfaceVariant),
|
||||
contentDescription = ""
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
) {
|
||||
if (reviews?.isNotEmpty() == true) {
|
||||
reviews.reversed().forEachIndexed { index, review ->
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(end = 16.dp),
|
||||
verticalAlignment = Alignment.Top,
|
||||
horizontalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
AvatarImage(
|
||||
size = 50.dp,
|
||||
author = review.authorDetails
|
||||
)
|
||||
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp)
|
||||
) {
|
||||
Text(
|
||||
text = review.author,
|
||||
color = MaterialTheme.colorScheme.secondary,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp)
|
||||
) {
|
||||
Text(
|
||||
text = review.author,
|
||||
color = MaterialTheme.colorScheme.secondary,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
|
||||
HtmlText(
|
||||
text = review.content,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
HtmlText(
|
||||
text = review.content,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
|
||||
val createdAt = TmdbUtils.formatDate(review.createdAt)
|
||||
val updatedAt = TmdbUtils.formatDate(review.updatedAt)
|
||||
var timestamp = stringResource(id = R.string.created_at_label, createdAt)
|
||||
if (updatedAt != createdAt) {
|
||||
timestamp += "\n${stringResource(id = R.string.updated_at_label, updatedAt)}"
|
||||
}
|
||||
Text(
|
||||
text = timestamp,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
fontSize = 12.sp
|
||||
)
|
||||
}
|
||||
}
|
||||
if (index != reviews.size - 1) {
|
||||
Divider(
|
||||
color = MaterialTheme.colorScheme.secondary,
|
||||
modifier = Modifier.padding(vertical = 12.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.wrapContentHeight()
|
||||
.padding(horizontal = 24.dp, vertical = 22.dp),
|
||||
text = stringResource(R.string.no_reviews_label),
|
||||
color = MaterialTheme.colorScheme.tertiary,
|
||||
textAlign = TextAlign.Center,
|
||||
style = MaterialTheme.typography.headlineMedium
|
||||
)
|
||||
}
|
||||
}
|
||||
val createdAt = TmdbUtils.formatDate(review.createdAt)
|
||||
val updatedAt = TmdbUtils.formatDate(review.updatedAt)
|
||||
var timestamp = stringResource(id = R.string.created_at_label, createdAt)
|
||||
if (updatedAt != createdAt) {
|
||||
timestamp += "\n${stringResource(id = R.string.updated_at_label, updatedAt)}"
|
||||
}
|
||||
Text(
|
||||
text = timestamp,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
fontSize = 12.sp
|
||||
)
|
||||
}
|
||||
}
|
||||
if (index != reviews.size - 1) {
|
||||
Divider(
|
||||
color = MaterialTheme.colorScheme.secondary,
|
||||
modifier = Modifier.padding(vertical = 12.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.wrapContentHeight()
|
||||
.padding(horizontal = 24.dp, vertical = 22.dp),
|
||||
text = stringResource(R.string.no_reviews_label),
|
||||
color = MaterialTheme.colorScheme.tertiary,
|
||||
textAlign = TextAlign.Center,
|
||||
style = MaterialTheme.typography.headlineMedium
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@@ -1333,4 +1449,5 @@ fun DetailsFor(
|
||||
mainViewModel = mainViewModel
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -135,14 +135,11 @@ fun PersonDetailScreen(
|
||||
elevation = 0.dp
|
||||
)
|
||||
|
||||
val externalIdsMap = remember { mainViewModel.peopleExternalIdsMap }
|
||||
val externalIds = externalIdsMap[personId]
|
||||
externalIds?.let {
|
||||
ExternalIdsArea(
|
||||
externalIds = it,
|
||||
modifier = Modifier.padding(start = 4.dp)
|
||||
)
|
||||
}
|
||||
ExternalIdsArea(
|
||||
modifier = Modifier.padding(start = 4.dp),
|
||||
type = MediaViewType.PERSON,
|
||||
itemId = personId
|
||||
)
|
||||
|
||||
BiographyCard(person = person)
|
||||
|
||||
|
||||
@@ -176,7 +176,12 @@ class MainViewModel: ViewModel(), KoinComponent {
|
||||
}
|
||||
|
||||
fun produceExternalIdsFor(type: MediaViewType): Map<Int, ExternalIds> {
|
||||
return providesForType(type, { movieExternalIds }, { tvExternalIds })
|
||||
return when (type) {
|
||||
MediaViewType.MOVIE -> movieExternalIds
|
||||
MediaViewType.TV -> tvExternalIds
|
||||
MediaViewType.PERSON -> peopleExternalIdsMap
|
||||
else -> throw ViewableMediaTypeException(type)
|
||||
}
|
||||
}
|
||||
|
||||
fun produceKeywordsFor(type: MediaViewType): Map<Int, List<Keyword>> {
|
||||
@@ -263,6 +268,47 @@ class MainViewModel: ViewModel(), KoinComponent {
|
||||
}
|
||||
}
|
||||
|
||||
fun produceDetailsLoadingStateFor(mediaType: MediaViewType): MutableState<LoadingState> {
|
||||
return providesForType(mediaType, { movieDetailsLoadingState }, { tvDetailsLoadingState})
|
||||
}
|
||||
|
||||
fun produceImagesLoadingStateFor(mediaType: MediaViewType): MutableState<LoadingState> {
|
||||
return providesForType(mediaType, { movieImagesLoadingState }, { tvImagesLoadingState })
|
||||
}
|
||||
|
||||
fun produceCastCrewLoadingStateFor(mediaType: MediaViewType): MutableState<LoadingState> {
|
||||
return providesForType(mediaType, { movieCastCrewLoadingState }, { tvCastCrewLoadingState })
|
||||
}
|
||||
|
||||
fun produceVideosLoadingStateFor(mediaType: MediaViewType): MutableState<LoadingState> {
|
||||
return providesForType(mediaType, { movieVideosLoadingState }, { tvVideosLoadingState })
|
||||
}
|
||||
|
||||
fun produceReviewsLoadingStateFor(mediaType: MediaViewType): MutableState<LoadingState> {
|
||||
return providesForType(mediaType, { movieReviewsLoadingState }, { tvReviewsLoadingState })
|
||||
}
|
||||
|
||||
fun produceKeywordsLoadingStateFor(mediaType: MediaViewType): MutableState<LoadingState> {
|
||||
return providesForType(mediaType, { movieKeywordsLoadingState }, { tvKeywordsLoadingState })
|
||||
}
|
||||
|
||||
fun produceWatchProvidersLoadingStateFor(mediaType: MediaViewType): MutableState<LoadingState> {
|
||||
return providesForType(mediaType, { movieWatchProvidersLoadingState }, { tvWatchProvidersLoadingState })
|
||||
}
|
||||
|
||||
fun produceExternalIdsLoadingStateFor(mediaType: MediaViewType): MutableState<LoadingState> {
|
||||
return when (mediaType) {
|
||||
MediaViewType.MOVIE -> movieExternalIdsLoadingState
|
||||
MediaViewType.TV -> tvExternalIdsLoadingState
|
||||
MediaViewType.PERSON -> peopleExternalIdsLoadingState
|
||||
else -> throw ViewableMediaTypeException(mediaType)
|
||||
}
|
||||
}
|
||||
|
||||
fun produceAccountStatesLoadingStateFor(mediaType: MediaViewType): MutableState<LoadingState> {
|
||||
return providesForType(mediaType, { movieAccountStatesLoadingState }, { tvAccountStatesLoadingState })
|
||||
}
|
||||
|
||||
suspend fun getById(id: Int, type: MediaViewType, force: Boolean = false) {
|
||||
when (type) {
|
||||
MediaViewType.MOVIE -> if (detailMovies[id] == null || force) movieService.getById(id, force)
|
||||
@@ -453,4 +499,43 @@ class MainViewModel: ViewModel(), KoinComponent {
|
||||
peopleExternalIds.value == LoadingState.REFRESHING
|
||||
}
|
||||
|
||||
@SuppressLint("ComposableNaming")
|
||||
@Composable
|
||||
fun monitorDetailsLoading(loading: MutableState<Boolean>) {
|
||||
val movieDetails = remember { movieDetailsLoadingState }
|
||||
val movieImages = remember { movieImagesLoadingState }
|
||||
val movieCastCrew = remember { movieCastCrewLoadingState }
|
||||
val movieVideos = remember { movieVideosLoadingState }
|
||||
val movieReviews = remember { movieReviewsLoadingState }
|
||||
val movieKeywords = remember { movieKeywordsLoadingState }
|
||||
val movieWatchProviders = remember { movieWatchProvidersLoadingState }
|
||||
val movieExternalIds = remember { movieExternalIdsLoadingState }
|
||||
val movieReleaseDates = remember { movieReleaseDatesLoadingState }
|
||||
val movieAccountStates = remember { movieAccountStatesLoadingState }
|
||||
val tvDetails = remember { tvDetailsLoadingState }
|
||||
val tvImages = remember { tvImagesLoadingState }
|
||||
val tvCastCrew = remember { tvCastCrewLoadingState }
|
||||
val tvVideos = remember { tvVideosLoadingState }
|
||||
val tvReviews = remember { tvReviewsLoadingState }
|
||||
val tvKeywords = remember { tvKeywordsLoadingState }
|
||||
val tvWatchProviders = remember { tvWatchProvidersLoadingState }
|
||||
val tvExternalIds = remember { tvExternalIdsLoadingState }
|
||||
val tvSeasons = remember { tvSeasonsLoadingState }
|
||||
val tvContentRatings = remember { tvContentRatingsLoadingState }
|
||||
val tvAccountStates = remember { tvAccountStatesLoadingState }
|
||||
val peopleDetails = remember { peopleDetailsLoadingState }
|
||||
val peopleCastCrew = remember { peopleCastCrewLoadingState }
|
||||
val peopleImages = remember { peopleImagesLoadingState }
|
||||
val peopleExternalIds = remember { peopleExternalIdsLoadingState }
|
||||
|
||||
loading.value = listOf(
|
||||
movieDetails.value, movieImages.value, movieCastCrew.value, movieVideos.value, movieReviews.value,
|
||||
movieKeywords.value, movieWatchProviders.value, movieExternalIds.value, movieReleaseDates.value,
|
||||
movieAccountStates.value, tvDetails.value, tvImages.value, tvCastCrew.value, tvVideos.value,
|
||||
tvReviews.value, tvKeywords.value, tvWatchProviders.value, tvExternalIds.value, tvSeasons.value,
|
||||
tvContentRatings.value, tvAccountStates.value, peopleDetails.value, peopleCastCrew.value,
|
||||
peopleImages.value, peopleExternalIds.value
|
||||
).any { it == LoadingState.LOADING || it == LoadingState.REFRESHING }
|
||||
}
|
||||
|
||||
}
|
||||
@@ -104,8 +104,8 @@ object TmdbUtils {
|
||||
return "${startYear}-${endYear}"
|
||||
}
|
||||
|
||||
fun convertRuntimeToHoursMinutes(movie: DetailedMovie): String {
|
||||
movie.runtime?.let { runtime ->
|
||||
fun convertRuntimeToHoursMinutes(movie: DetailedMovie?): String {
|
||||
movie?.runtime?.let { runtime ->
|
||||
return convertRuntimeToHoursAndMinutes(runtime)
|
||||
}
|
||||
return ""
|
||||
|
||||
Reference in New Issue
Block a user