begin skeleton loading imeplementation

This commit is contained in:
Owen LeJeune
2023-07-14 23:15:25 -04:00
parent 862dfb2341
commit 7fb31cb041
12 changed files with 599 additions and 274 deletions

View File

@@ -1,6 +1,7 @@
package com.owenlejeune.tvtime.api package com.owenlejeune.tvtime.api
import androidx.compose.runtime.MutableState import androidx.compose.runtime.MutableState
import kotlinx.coroutines.delay
import retrofit2.Response import retrofit2.Response
infix fun <T> Response<T>.storedIn(body: (T) -> Unit) { infix fun <T> Response<T>.storedIn(body: (T) -> Unit) {

View File

@@ -5,4 +5,17 @@ import com.google.gson.annotations.SerializedName
data class Genre( data class Genre(
@SerializedName("id") val id: Int, @SerializedName("id") val id: Int,
@SerializedName("name") val name: String @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, " "))
}
}
}
}
}

View File

@@ -9,3 +9,5 @@ fun Any.coroutineTask(runnable: suspend () -> Unit) {
} }
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 }

View File

@@ -17,7 +17,10 @@ import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.graphics.Shape import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.graphics.TileMode 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 transition = rememberInfiniteTransition()
val translateAnimation by transition.animateFloat( val translateAnimation by transition.animateFloat(
initialValue = 0f, initialValue = 0f,
@@ -28,8 +31,8 @@ fun Modifier.shimmerBackground(shape: Shape = RectangleShape): Modifier = compos
), ),
) )
val shimmerColors = listOf( val shimmerColors = listOf(
Color.LightGray.copy(alpha = 0.9f), tint.copy(alpha = 0.9f),
Color.LightGray.copy(alpha = 0.4f), tint.copy(alpha = 0.4f),
) )
val brush = Brush.linearGradient( val brush = Brush.linearGradient(
colors = shimmerColors, colors = shimmerColors,

View File

@@ -29,10 +29,14 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel 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.FavoriteSelected
import com.owenlejeune.tvtime.ui.theme.RatingSelected import com.owenlejeune.tvtime.ui.theme.RatingSelected
import com.owenlejeune.tvtime.ui.theme.WatchlistSelected import com.owenlejeune.tvtime.ui.theme.WatchlistSelected
import com.owenlejeune.tvtime.ui.viewmodel.AccountViewModel 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.ui.viewmodel.MainViewModel
import com.owenlejeune.tvtime.utils.types.MediaViewType import com.owenlejeune.tvtime.utils.types.MediaViewType
import kotlinx.coroutines.launch import kotlinx.coroutines.launch

View File

@@ -6,12 +6,15 @@ import androidx.compose.foundation.Image
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.* 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.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
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.Brush
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.painter.Painter 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.Dp
import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import coil.compose.AsyncImage import coil.compose.AsyncImage
import coil.compose.rememberAsyncImagePainter import coil.compose.rememberAsyncImagePainter
import coil.request.CachePolicy 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.PagerState
import com.google.accompanist.pager.rememberPagerState import com.google.accompanist.pager.rememberPagerState
import com.owenlejeune.tvtime.R 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.ExternalIds
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.ImageCollection 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.TmdbUtils
import com.owenlejeune.tvtime.utils.types.MediaViewType
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@@ -54,7 +63,9 @@ fun DetailHeader(
) { ) {
Box( Box(
modifier = modifier.then( modifier = modifier.then(
Modifier.fillMaxWidth().wrapContentHeight() Modifier
.fillMaxWidth()
.wrapContentHeight()
) )
) { ) {
if (imageCollection != null) { if (imageCollection != null) {
@@ -119,7 +130,8 @@ private fun BackdropContainer(
Box( Box(
modifier = Modifier modifier = Modifier
.matchParentSize() .width(sizeImage.value.width.toDp())
.height(sizeImage.value.height.toDp())
.background(gradient) .background(gradient)
) )
} }
@@ -214,10 +226,35 @@ fun BackdropGallery(
@Composable @Composable
fun ExternalIdsArea( fun ExternalIdsArea(
externalIds: ExternalIds, type: MediaViewType,
itemId: Int,
modifier: Modifier = Modifier 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( Row(
modifier = modifier, modifier = modifier,
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
@@ -265,3 +302,31 @@ private fun ExternalIdLogo(
) )
} }
} }
@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)
)
}
}
}

View File

@@ -172,14 +172,15 @@ fun PosterItem(
var sizeImage by remember { mutableStateOf(IntSize.Zero) } var sizeImage by remember { mutableStateOf(IntSize.Zero) }
Card( Card(
elevation = CardDefaults.elevatedCardElevation(defaultElevation = elevation), elevation = CardDefaults.elevatedCardElevation(defaultElevation = elevation),
modifier = modifier modifier = modifier.then(Modifier
.width(width = width) .width(width = width)
.height(height = height) .height(height = height)
.wrapContentHeight() .wrapContentHeight()
.clickable( .clickable(
enabled = enabled, enabled = enabled,
onClick = onClick onClick = onClick
), )
),
shape = RoundedCornerShape(5.dp), shape = RoundedCornerShape(5.dp),
colors = CardDefaults.cardColors(containerColor = Color.Transparent) colors = CardDefaults.cardColors(containerColor = Color.Transparent)
) { ) {
@@ -259,3 +260,20 @@ 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)
)
)
}

View File

@@ -68,6 +68,7 @@ import coil.request.ImageRequest
import com.google.accompanist.flowlayout.FlowRow import com.google.accompanist.flowlayout.FlowRow
import com.owenlejeune.tvtime.R import com.owenlejeune.tvtime.R
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.AuthorDetails 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.toDp
import com.owenlejeune.tvtime.extensions.unlessEmpty import com.owenlejeune.tvtime.extensions.unlessEmpty
import com.owenlejeune.tvtime.preferences.AppPreferences import com.owenlejeune.tvtime.preferences.AppPreferences
@@ -285,7 +286,17 @@ class ChipInfo(
val text: String, val text: String,
val enabled: Boolean = true, val enabled: Boolean = true,
val id: Int? = null 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) { sealed class ChipStyle(val mainAxisSpacing: Dp, val crossAxisSpacing: Dp) {
object Boxy: ChipStyle(8.dp, 4.dp) object Boxy: ChipStyle(8.dp, 4.dp)
@@ -338,15 +349,17 @@ fun BoxyChip(
style: TextStyle = MaterialTheme.typography.bodySmall, style: TextStyle = MaterialTheme.typography.bodySmall,
isSelected: Boolean = true, isSelected: Boolean = true,
onSelectionChanged: (ChipInfo) -> Unit = {}, 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( Surface(
shadowElevation = 2.dp, shadowElevation = 2.dp,
shape = RoundedCornerShape(5.dp), shape = RoundedCornerShape(5.dp),
color = if (isSelected) colors.selectedContainerColor() else colors.unselectedContainerColor() color = if (isSelected) colors.selectedContainerColor() else colors.unselectedContainerColor()
) { ) {
Row( Row(
modifier = Modifier modifier = shimmerModifier
.toggleable( .toggleable(
value = isSelected, value = isSelected,
onValueChange = { onValueChange = {
@@ -367,21 +380,25 @@ fun BoxyChip(
@Composable @Composable
fun RoundedChip( fun RoundedChip(
modifier: Modifier = Modifier,
chipInfo: ChipInfo, chipInfo: ChipInfo,
style: TextStyle = MaterialTheme.typography.bodySmall, style: TextStyle = MaterialTheme.typography.bodySmall,
isSelected: Boolean = false, isSelected: Boolean = false,
onSelectionChanged: (ChipInfo) -> Unit = {}, onSelectionChanged: (ChipInfo) -> Unit = {},
colors: ChipColors = ChipDefaults.roundedChipColors() colors: ChipColors = ChipDefaults.roundedChipColors(),
isLoading: Boolean = false
) { ) {
val borderColor = if (isSelected) colors.selectedContainerColor() else colors.unselectedContainerColor() val borderColor = if (isSelected) colors.selectedContainerColor() else colors.unselectedContainerColor()
val radius = style.fontSize.value.dp * 2 val radius = style.fontSize.value.dp * 2
Surface( Surface(
modifier = modifier,
border = BorderStroke(width = 1.dp, borderColor), border = BorderStroke(width = 1.dp, borderColor),
shape = RoundedCornerShape(radius), shape = RoundedCornerShape(radius),
color = MaterialTheme.colorScheme.surfaceVariant color = MaterialTheme.colorScheme.surfaceVariant
) { ) {
val shimmerModifier = if(isLoading) Modifier.shimmerBackground(RoundedCornerShape(radius)) else Modifier
Row( Row(
modifier = Modifier modifier = shimmerModifier
.toggleable( .toggleable(
value = isSelected, value = isSelected,
onValueChange = { onValueChange = {
@@ -403,6 +420,7 @@ fun RoundedChip(
@Composable @Composable
fun ChipGroup( fun ChipGroup(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
isLoading: Boolean = false,
chips: List<ChipInfo> = emptyList(), chips: List<ChipInfo> = emptyList(),
onSelectedChanged: (ChipInfo) -> Unit = {}, onSelectedChanged: (ChipInfo) -> Unit = {},
chipStyle: ChipStyle = ChipStyle.Boxy, chipStyle: ChipStyle = ChipStyle.Boxy,
@@ -411,24 +429,26 @@ fun ChipGroup(
) { ) {
@Composable @Composable
fun DrawChip(chipStyle: ChipStyle, chip: ChipInfo) { fun DrawChip(chipStyle: ChipStyle, chip: ChipInfo, isLoading: Boolean) {
when (chipStyle) { when (chipStyle) {
ChipStyle.Boxy -> { ChipStyle.Boxy -> {
BoxyChip( BoxyChip(
chipInfo = chip, chipInfo = chip,
onSelectionChanged = onSelectedChanged, onSelectionChanged = onSelectedChanged,
colors = boxyChipColors colors = boxyChipColors,
isLoading = isLoading
) )
} }
ChipStyle.Rounded -> { ChipStyle.Rounded -> {
RoundedChip( RoundedChip(
chipInfo = chip, chipInfo = chip,
onSelectionChanged = onSelectedChanged, onSelectionChanged = onSelectedChanged,
colors = roundedChipColors colors = roundedChipColors,
isLoading = isLoading
) )
} }
is ChipStyle.Mixed -> { 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 mainAxisSpacing = chipStyle.mainAxisSpacing
) { ) {
chips.forEach { chip -> chips.forEach { chip ->
DrawChip(chipStyle = chipStyle, chip = chip) DrawChip(chipStyle = chipStyle, chip = chip, isLoading = isLoading)
} }
} }
} }

View File

@@ -47,7 +47,6 @@ import androidx.compose.runtime.MutableState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier 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.PagerState
import com.google.accompanist.pager.rememberPagerState import com.google.accompanist.pager.rememberPagerState
import com.owenlejeune.tvtime.R 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.DetailedItem
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.DetailedMovie import com.owenlejeune.tvtime.api.tmdb.api.v3.model.DetailedMovie
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.DetailedTv 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.WindowSizeClass
import com.owenlejeune.tvtime.extensions.format import com.owenlejeune.tvtime.extensions.format
import com.owenlejeune.tvtime.extensions.getCalendarYear import com.owenlejeune.tvtime.extensions.getCalendarYear
import com.owenlejeune.tvtime.extensions.isIn
import com.owenlejeune.tvtime.extensions.lazyPagingItems import com.owenlejeune.tvtime.extensions.lazyPagingItems
import com.owenlejeune.tvtime.extensions.listItems import com.owenlejeune.tvtime.extensions.listItems
import com.owenlejeune.tvtime.extensions.shimmerBackground
import com.owenlejeune.tvtime.preferences.AppPreferences import com.owenlejeune.tvtime.preferences.AppPreferences
import com.owenlejeune.tvtime.ui.components.ActionsView import com.owenlejeune.tvtime.ui.components.ActionsView
import com.owenlejeune.tvtime.ui.components.AvatarImage 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.ImageGalleryOverlay
import com.owenlejeune.tvtime.ui.components.ListContentCard import com.owenlejeune.tvtime.ui.components.ListContentCard
import com.owenlejeune.tvtime.ui.components.PillSegmentedControl 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.PosterItem
import com.owenlejeune.tvtime.ui.components.RoundedChip import com.owenlejeune.tvtime.ui.components.RoundedChip
import com.owenlejeune.tvtime.ui.components.RoundedTextField 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.SessionManager
import com.owenlejeune.tvtime.utils.TmdbUtils import com.owenlejeune.tvtime.utils.TmdbUtils
import com.owenlejeune.tvtime.utils.types.MediaViewType import com.owenlejeune.tvtime.utils.types.MediaViewType
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.koin.java.KoinJavaComponent.get import org.koin.java.KoinJavaComponent
private suspend fun fetchData( private fun fetchData(
mainViewModel: MainViewModel, mainViewModel: MainViewModel,
itemId: Int, itemId: Int,
type: MediaViewType, type: MediaViewType,
force: Boolean = false force: Boolean = false
) { ) {
mainViewModel.getById(itemId, type, force) val scope = CoroutineScope(Dispatchers.IO)
mainViewModel.getImages(itemId, type, force)
mainViewModel.getExternalIds(itemId, type, force) scope.launch { mainViewModel.getById(itemId, type, force) }
mainViewModel.getKeywords(itemId, type, force) scope.launch { mainViewModel.getImages(itemId, type, force) }
mainViewModel.getCastAndCrew(itemId, type, force) scope.launch { mainViewModel.getExternalIds(itemId, type, force) }
mainViewModel.getSimilar(itemId, type) scope.launch { mainViewModel.getKeywords(itemId, type, force) }
mainViewModel.getVideos(itemId, type, force) scope.launch { mainViewModel.getCastAndCrew(itemId, type, force) }
mainViewModel.getWatchProviders(itemId, type, force) scope.launch { mainViewModel.getSimilar(itemId, type) }
mainViewModel.getReviews(itemId, type, force) 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) { when (type) {
MediaViewType.MOVIE -> { MediaViewType.MOVIE -> {
mainViewModel.getReleaseDates(itemId, force) scope.launch { mainViewModel.getReleaseDates(itemId, force) }
} }
MediaViewType.TV -> { MediaViewType.TV -> {
mainViewModel.getContentRatings(itemId, force) scope.launch { mainViewModel.getContentRatings(itemId, force) }
} }
else -> {} else -> {}
} }
@@ -159,8 +168,6 @@ fun MediaDetailScreen(
type: MediaViewType, type: MediaViewType,
windowSize: WindowSizeClass windowSize: WindowSizeClass
) { ) {
val scope = rememberCoroutineScope()
val mainViewModel = viewModel<MainViewModel>() val mainViewModel = viewModel<MainViewModel>()
val applicationViewModel = viewModel<ApplicationViewModel>() val applicationViewModel = viewModel<ApplicationViewModel>()
@@ -193,12 +200,11 @@ fun MediaDetailScreen(
val isRefreshing = remember { mutableStateOf(false) } val isRefreshing = remember { mutableStateOf(false) }
mainViewModel.monitorDetailsLoadingRefreshing(refreshing = isRefreshing) mainViewModel.monitorDetailsLoadingRefreshing(refreshing = isRefreshing)
val pullRefreshState = rememberPullRefreshState( val pullRefreshState = rememberPullRefreshState(
refreshing = isRefreshing.value, refreshing = isRefreshing.value,
onRefresh = { onRefresh = {
scope.launch { fetchData(mainViewModel, itemId, type, true)
fetchData(mainViewModel, itemId, type, true)
}
} }
) )
@@ -232,7 +238,7 @@ fun MediaDetailScreen(
windowSize = windowSize, windowSize = windowSize,
showImageGallery = showGalleryOverlay, showImageGallery = showGalleryOverlay,
pagerState = pagerState, pagerState = pagerState,
mainViewModel = mainViewModel mainViewModel = mainViewModel
) )
PullRefreshIndicator( PullRefreshIndicator(
refreshing = isRefreshing.value, refreshing = isRefreshing.value,
@@ -254,9 +260,9 @@ fun MediaDetailScreen(
} }
} }
@OptIn(ExperimentalPagerApi::class, ExperimentalMaterialApi::class) @OptIn(ExperimentalPagerApi::class)
@Composable @Composable
private fun MediaViewContent( fun MediaViewContent(
appNavController: NavController, appNavController: NavController,
mainViewModel: MainViewModel, mainViewModel: MainViewModel,
itemId: Int, itemId: Int,
@@ -266,7 +272,7 @@ private fun MediaViewContent(
windowSize: WindowSizeClass, windowSize: WindowSizeClass,
showImageGallery: MutableState<Boolean>, showImageGallery: MutableState<Boolean>,
pagerState: PagerState, pagerState: PagerState,
preferences: AppPreferences = get(AppPreferences::class.java) preferences: AppPreferences = KoinJavaComponent.get(AppPreferences::class.java)
) { ) {
Row( Row(
modifier = Modifier modifier = Modifier
@@ -280,15 +286,22 @@ private fun MediaViewContent(
.verticalScroll(state = rememberScrollState()), .verticalScroll(state = rememberScrollState()),
verticalArrangement = Arrangement.spacedBy(16.dp) verticalArrangement = Arrangement.spacedBy(16.dp)
) { ) {
DetailHeader( val detailsLoadingState = remember { mainViewModel.produceDetailsLoadingStateFor(type) }
posterUrl = TmdbUtils.getFullPosterPath(mediaItem?.posterPath), val isLoading = detailsLoadingState.value.isIn(LoadingState.LOADING, LoadingState.REFRESHING)
posterContentDescription = mediaItem?.title,
backdropUrl = TmdbUtils.getFullBackdropPath(mediaItem?.backdropPath), if (isLoading) {
rating = mediaItem?.voteAverage?.let { it / 10 }, PlaceholderDetailHeader()
imageCollection = images, } else {
showGalleryOverlay = showImageGallery, DetailHeader(
pagerState = pagerState 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( Column(
verticalArrangement = Arrangement.spacedBy(16.dp) verticalArrangement = Arrangement.spacedBy(16.dp)
@@ -300,13 +313,11 @@ private fun MediaViewContent(
itemId = itemId itemId = itemId
) )
val externalIdsMap = remember { mainViewModel.produceExternalIdsFor(type) } ExternalIdsArea(
externalIdsMap[itemId]?.let { modifier = Modifier.padding(start = 20.dp),
ExternalIdsArea( type = type,
externalIds = it, itemId = itemId
modifier = Modifier.padding(start = 20.dp) )
)
}
val currentSession = remember { SessionManager.currentSession } val currentSession = remember { SessionManager.currentSession }
currentSession.value?.let { currentSession.value?.let {
@@ -409,7 +420,8 @@ private fun MiscTvDetails(
year = TmdbUtils.getSeriesRun(series), year = TmdbUtils.getSeriesRun(series),
runtime = TmdbUtils.convertRuntimeToHoursMinutes(series), runtime = TmdbUtils.convertRuntimeToHoursMinutes(series),
genres = series.genres, genres = series.genres,
contentRating = contentRating contentRating = contentRating,
type = MediaViewType.TV
) )
} }
} }
@@ -420,23 +432,22 @@ private fun MiscMovieDetails(
mediaItem: DetailedItem?, mediaItem: DetailedItem?,
mainViewModel: MainViewModel mainViewModel: MainViewModel
) { ) {
mediaItem?.let { mi -> val movie = mediaItem as? DetailedMovie
val movie = mi as DetailedMovie
val contentRatingsMap = remember { mainViewModel.movieReleaseDates } val contentRatingsMap = remember { mainViewModel.movieReleaseDates }
val contentRating = TmdbUtils.getMovieRating(contentRatingsMap[itemId]) val contentRating = TmdbUtils.getMovieRating(contentRatingsMap[itemId])
MiscDetails( MiscDetails(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.wrapContentHeight() .wrapContentHeight()
.padding(horizontal = 16.dp), .padding(horizontal = 16.dp),
year = movie.releaseDate?.getCalendarYear()?.toString() ?: "", year = movie?.releaseDate?.getCalendarYear()?.toString() ?: "",
runtime = TmdbUtils.convertRuntimeToHoursMinutes(movie), runtime = TmdbUtils.convertRuntimeToHoursMinutes(movie),
genres = movie.genres, genres = movie?.genres ?: emptyList(),
contentRating = contentRating contentRating = contentRating,
) type = MediaViewType.MOVIE
} )
} }
@Composable @Composable
@@ -445,8 +456,19 @@ private fun MiscDetails(
year: String, year: String,
runtime: String, runtime: String,
genres: List<Genre>, 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( Column(
modifier = modifier, modifier = modifier,
verticalArrangement = Arrangement.spacedBy(8.dp) verticalArrangement = Arrangement.spacedBy(8.dp)
@@ -454,28 +476,35 @@ private fun MiscDetails(
Row( Row(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.wrapContentHeight() .wrapContentHeight(),
horizontalArrangement = Arrangement.spacedBy(if (isLoading) 6.dp else 0.dp)
) { ) {
Text(text = year, color = MaterialTheme.colorScheme.onBackground) Text(
if (runtime != "0m") { text = year.takeUnless { isLoading } ?: "",
color = MaterialTheme.colorScheme.onBackground,
modifier = shimmerModifier
)
if (runtime != "0m" || isLoading) {
Text( Text(
text = runtime, text = runtime.takeUnless { isLoading } ?: "",
color = MaterialTheme.colorScheme.onBackground, color = MaterialTheme.colorScheme.onBackground,
modifier = Modifier.padding(start = 12.dp) modifier = shimmerModifier.padding(start = 12.dp)
) )
} }
Text( Text(
text = contentRating, text = contentRating.takeUnless { isLoading } ?: "",
color = MaterialTheme.colorScheme.onBackground, 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( ChipGroup(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.wrapContentHeight(), .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 keywordsMap = remember { mainViewModel.produceKeywordsFor(type) }
val keywords = keywordsMap[itemId] val keywords = keywordsMap[itemId]
mediaItem?.let { mi -> val detailsLoadingState = remember { mainViewModel.produceAccountStatesLoadingStateFor(type) }
if (!mi.tagline.isNullOrEmpty() || keywords?.isNotEmpty() == true || !mi.overview.isNullOrEmpty()) { val isLoading = detailsLoadingState.value.isIn(LoadingState.LOADING, LoadingState.REFRESHING)
ContentCard(
modifier = modifier 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 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() .fillMaxWidth()
.wrapContentHeight(), .height(100.dp)
verticalArrangement = Arrangement.spacedBy(8.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)) Spacer(modifier = Modifier.width(8.dp))
mi.tagline?.let { tagline -> ChipInfo.generatePlaceholders(4).forEach { keywordChipInfo ->
if (tagline.isNotEmpty()) { RoundedChip(
Text( modifier = Modifier.width(60.dp),
modifier = Modifier.padding(horizontal = 16.dp), isLoading = true,
text = tagline, chipInfo = keywordChipInfo,
color = MaterialTheme.colorScheme.tertiary colors = ChipDefaults.roundedChipColors(
, unselectedContainerColor = MaterialTheme.colorScheme.primary,
style = MaterialTheme.typography.bodyLarge, unselectedContentColor = MaterialTheme.colorScheme.primary
fontStyle = FontStyle.Italic, ),
) onSelectionChanged = {}
} )
} }
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( } else {
modifier = Modifier.horizontalScroll(rememberScrollState()), mediaItem?.let { mi ->
horizontalArrangement = Arrangement.spacedBy(ChipStyle.Rounded.mainAxisSpacing) if (!mi.tagline.isNullOrEmpty() || keywords?.isNotEmpty() == true || !mi.overview.isNullOrEmpty()) {
) { ContentCard(
Spacer(modifier = Modifier.width(8.dp)) modifier = modifier
keywordsChipInfo.forEach { keywordChipInfo -> ) {
RoundedChip( Column(
chipInfo = keywordChipInfo, modifier = Modifier
colors = ChipDefaults.roundedChipColors( .fillMaxWidth()
unselectedContainerColor = MaterialTheme.colorScheme.primary, .wrapContentHeight(),
unselectedContentColor = MaterialTheme.colorScheme.primary verticalArrangement = Arrangement.spacedBy(8.dp)
), ) {
onSelectionChanged = { chip -> Spacer(modifier = Modifier.height(8.dp))
appNavController.navigate(AppNavItem.KeywordsView.withArgs(type, chip.text, chip.id!!)) 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 castMap = remember { mainViewModel.produceCastFor(type) }
val cast = castMap[itemId] val cast = castMap[itemId]
val loadingState = remember { mainViewModel.produceDetailsLoadingStateFor(type) }
val isLoading = loadingState.value.isIn(LoadingState.LOADING, LoadingState.REFRESHING)
ContentCard( ContentCard(
modifier = modifier, modifier = modifier,
title = stringResource(R.string.cast_label), title = stringResource(R.string.cast_label),
@@ -718,9 +815,15 @@ private fun CastCard(
item { item {
Spacer(modifier = Modifier.width(8.dp)) Spacer(modifier = Modifier.width(8.dp))
} }
items(cast?.size ?: 0) { i -> if (isLoading) {
cast?.get(i)?.let { items(5) {
CastCrewCard(appNavController = appNavController, person = it) PlaceholderPosterItem()
}
} else {
items(cast?.size ?: 0) { i ->
cast?.get(i)?.let {
CastCrewCard(appNavController = appNavController, person = it)
}
} }
} }
item { item {
@@ -728,27 +831,40 @@ private fun CastCard(
} }
} }
Text( if (isLoading) {
text = stringResource(R.string.see_all_cast_and_crew), Text(
fontSize = 12.sp, text = "",
color = MaterialTheme.colorScheme.inversePrimary, modifier = Modifier
modifier = Modifier .padding(start = 12.dp, bottom = 12.dp)
.padding(start = 12.dp, bottom = 12.dp) .width(80.dp)
.clickable { .shimmerBackground(RoundedCornerShape(10.dp))
appNavController.navigate( )
AppNavItem.CastCrewListView.withArgs( } else {
type, Text(
itemId 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 @Composable
private fun CastCrewCard(appNavController: NavController, person: Person) { private fun CastCrewCard(
appNavController: NavController,
person: Person
) {
TwoLineImageTextCard( TwoLineImageTextCard(
title = person.name, title = person.name,
modifier = Modifier modifier = Modifier
@@ -952,7 +1068,7 @@ private fun VideoGroup(results: List<Video>, type: Video.Type, title: String) {
val posterWidth = 120.dp val posterWidth = 120.dp
LazyRow(modifier = Modifier LazyRow(modifier = Modifier
.padding(vertical = 8.dp), .padding(vertical = 8.dp),
horizontalArrangement = Arrangement.spacedBy(4.dp) horizontalArrangement = Arrangement.spacedBy(4.dp)
) { ) {
item { item {
@@ -1193,124 +1309,124 @@ private fun ReviewsCard(
val reviewsMap = remember { mainViewModel.produceReviewsFor(type) } val reviewsMap = remember { mainViewModel.produceReviewsFor(type) }
val reviews = reviewsMap[itemId] val reviews = reviewsMap[itemId]
ListContentCard( ListContentCard(
modifier = modifier, modifier = modifier,
header = { header = {
Column( Column(
verticalArrangement = Arrangement.spacedBy(9.dp) verticalArrangement = Arrangement.spacedBy(9.dp)
) { ) {
Text( Text(
text = stringResource(R.string.reviews_title), text = stringResource(R.string.reviews_title),
style = MaterialTheme.typography.titleLarge, style = MaterialTheme.typography.titleLarge,
color = MaterialTheme.colorScheme.onSurfaceVariant color = MaterialTheme.colorScheme.onSurfaceVariant
) )
if (SessionManager.currentSession.value?.isAuthorized == true) { if (SessionManager.currentSession.value?.isAuthorized == true) {
Row( Row(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.height(50.dp) .height(50.dp)
.padding(bottom = 4.dp), .padding(bottom = 4.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp) horizontalArrangement = Arrangement.spacedBy(8.dp)
) { ) {
var reviewTextState by remember { mutableStateOf("") } var reviewTextState by remember { mutableStateOf("") }
RoundedTextField( RoundedTextField(
modifier = Modifier modifier = Modifier
.height(40.dp) .height(40.dp)
.align(Alignment.CenterVertically) .align(Alignment.CenterVertically)
.weight(1f), .weight(1f),
value = reviewTextState, value = reviewTextState,
onValueChange = { reviewTextState = it }, onValueChange = { reviewTextState = it },
placeHolder = stringResource(R.string.add_a_review_hint), placeHolder = stringResource(R.string.add_a_review_hint),
backgroundColor = MaterialTheme.colorScheme.secondary, backgroundColor = MaterialTheme.colorScheme.secondary,
placeHolderTextColor = MaterialTheme.colorScheme.background, placeHolderTextColor = MaterialTheme.colorScheme.background,
textColor = MaterialTheme.colorScheme.onSecondary textColor = MaterialTheme.colorScheme.onSecondary
) )
val context = LocalContext.current val context = LocalContext.current
CircleBackgroundColorImage( CircleBackgroundColorImage(
modifier = Modifier modifier = Modifier
.align(Alignment.CenterVertically) .align(Alignment.CenterVertically)
.clickable( .clickable(
onClick = { onClick = {
Toast Toast
.makeText(context, "TODO", Toast.LENGTH_SHORT) .makeText(context, "TODO", Toast.LENGTH_SHORT)
.show() .show()
} }
), ),
size = 40.dp, size = 40.dp,
backgroundColor = MaterialTheme.colorScheme.tertiary, backgroundColor = MaterialTheme.colorScheme.tertiary,
image = Icons.Filled.Send, image = Icons.Filled.Send,
colorFilter = ColorFilter.tint(color = MaterialTheme.colorScheme.surfaceVariant), colorFilter = ColorFilter.tint(color = MaterialTheme.colorScheme.surfaceVariant),
contentDescription = "" contentDescription = ""
) )
} }
} }
} }
}, },
) { ) {
if (reviews?.isNotEmpty() == true) { if (reviews?.isNotEmpty() == true) {
reviews.reversed().forEachIndexed { index, review -> reviews.reversed().forEachIndexed { index, review ->
Row( Row(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(end = 16.dp), .padding(end = 16.dp),
verticalAlignment = Alignment.Top, verticalAlignment = Alignment.Top,
horizontalArrangement = Arrangement.spacedBy(16.dp) horizontalArrangement = Arrangement.spacedBy(16.dp)
) { ) {
AvatarImage( AvatarImage(
size = 50.dp, size = 50.dp,
author = review.authorDetails author = review.authorDetails
) )
Column( Column(
verticalArrangement = Arrangement.spacedBy(4.dp) verticalArrangement = Arrangement.spacedBy(4.dp)
) { ) {
Text( Text(
text = review.author, text = review.author,
color = MaterialTheme.colorScheme.secondary, color = MaterialTheme.colorScheme.secondary,
fontWeight = FontWeight.Bold fontWeight = FontWeight.Bold
) )
HtmlText( HtmlText(
text = review.content, text = review.content,
color = MaterialTheme.colorScheme.onSurfaceVariant color = MaterialTheme.colorScheme.onSurfaceVariant
) )
val createdAt = TmdbUtils.formatDate(review.createdAt) val createdAt = TmdbUtils.formatDate(review.createdAt)
val updatedAt = TmdbUtils.formatDate(review.updatedAt) val updatedAt = TmdbUtils.formatDate(review.updatedAt)
var timestamp = stringResource(id = R.string.created_at_label, createdAt) var timestamp = stringResource(id = R.string.created_at_label, createdAt)
if (updatedAt != createdAt) { if (updatedAt != createdAt) {
timestamp += "\n${stringResource(id = R.string.updated_at_label, updatedAt)}" timestamp += "\n${stringResource(id = R.string.updated_at_label, updatedAt)}"
} }
Text( Text(
text = timestamp, text = timestamp,
color = MaterialTheme.colorScheme.onSurfaceVariant, color = MaterialTheme.colorScheme.onSurfaceVariant,
fontSize = 12.sp fontSize = 12.sp
) )
} }
} }
if (index != reviews.size - 1) { if (index != reviews.size - 1) {
Divider( Divider(
color = MaterialTheme.colorScheme.secondary, color = MaterialTheme.colorScheme.secondary,
modifier = Modifier.padding(vertical = 12.dp) modifier = Modifier.padding(vertical = 12.dp)
) )
} }
} }
} else { } else {
Text( Text(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.wrapContentHeight() .wrapContentHeight()
.padding(horizontal = 24.dp, vertical = 22.dp), .padding(horizontal = 24.dp, vertical = 22.dp),
text = stringResource(R.string.no_reviews_label), text = stringResource(R.string.no_reviews_label),
color = MaterialTheme.colorScheme.tertiary, color = MaterialTheme.colorScheme.tertiary,
textAlign = TextAlign.Center, textAlign = TextAlign.Center,
style = MaterialTheme.typography.headlineMedium style = MaterialTheme.typography.headlineMedium
) )
} }
} }
} }
@Composable @Composable
@@ -1334,3 +1450,4 @@ fun DetailsFor(
) )
} }
} }

View File

@@ -135,14 +135,11 @@ fun PersonDetailScreen(
elevation = 0.dp elevation = 0.dp
) )
val externalIdsMap = remember { mainViewModel.peopleExternalIdsMap } ExternalIdsArea(
val externalIds = externalIdsMap[personId] modifier = Modifier.padding(start = 4.dp),
externalIds?.let { type = MediaViewType.PERSON,
ExternalIdsArea( itemId = personId
externalIds = it, )
modifier = Modifier.padding(start = 4.dp)
)
}
BiographyCard(person = person) BiographyCard(person = person)

View File

@@ -176,7 +176,12 @@ class MainViewModel: ViewModel(), KoinComponent {
} }
fun produceExternalIdsFor(type: MediaViewType): Map<Int, ExternalIds> { 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>> { 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) { suspend fun getById(id: Int, type: MediaViewType, force: Boolean = false) {
when (type) { when (type) {
MediaViewType.MOVIE -> if (detailMovies[id] == null || force) movieService.getById(id, force) MediaViewType.MOVIE -> if (detailMovies[id] == null || force) movieService.getById(id, force)
@@ -453,4 +499,43 @@ class MainViewModel: ViewModel(), KoinComponent {
peopleExternalIds.value == LoadingState.REFRESHING 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 }
}
} }

View File

@@ -104,8 +104,8 @@ object TmdbUtils {
return "${startYear}-${endYear}" return "${startYear}-${endYear}"
} }
fun convertRuntimeToHoursMinutes(movie: DetailedMovie): String { fun convertRuntimeToHoursMinutes(movie: DetailedMovie?): String {
movie.runtime?.let { runtime -> movie?.runtime?.let { runtime ->
return convertRuntimeToHoursAndMinutes(runtime) return convertRuntimeToHoursAndMinutes(runtime)
} }
return "" return ""