add pull to refresh to details screens

This commit is contained in:
Owen LeJeune
2023-07-09 23:44:04 -04:00
parent 9877a50102
commit 2bdff383b7
12 changed files with 559 additions and 216 deletions

View File

@@ -4,6 +4,7 @@ enum class LoadingState {
INACTIVE,
LOADING,
REFRESHING,
COMPLETE,
ERROR

View File

@@ -1,5 +1,6 @@
package com.owenlejeune.tvtime.api
import androidx.compose.runtime.MutableState
import retrofit2.Response
infix fun <T> Response<T>.storedIn(body: (T) -> Unit) {
@@ -8,4 +9,24 @@ infix fun <T> Response<T>.storedIn(body: (T) -> Unit) {
body(it)
}
}
}
suspend fun <T> loadRemoteData(
fetcher: suspend () -> Response<T>,
processor: (T) -> Unit,
loadSate: MutableState<LoadingState>,
refreshing: Boolean
) {
loadSate.value = if (refreshing) LoadingState.REFRESHING else LoadingState.LOADING
val response = fetcher()
if (response.isSuccessful) {
response.body()?.let {
processor(it)
loadSate.value = LoadingState.COMPLETE
} ?: run {
loadSate.value = LoadingState.ERROR
}
} else {
loadSate.value = LoadingState.ERROR
}
}

View File

@@ -5,27 +5,27 @@ import retrofit2.Response
interface DetailService {
suspend fun getById(id: Int)
suspend fun getById(id: Int, refreshing: Boolean)
suspend fun getImages(id: Int)
suspend fun getImages(id: Int, refreshing: Boolean)
suspend fun getCastAndCrew(id: Int)
suspend fun getCastAndCrew(id: Int, refreshing: Boolean)
suspend fun getSimilar(id: Int, page: Int): Response<out HomePageResponse>
suspend fun getVideos(id: Int)
suspend fun getVideos(id: Int, refreshing: Boolean)
suspend fun getReviews(id: Int)
suspend fun getReviews(id: Int, refreshing: Boolean)
suspend fun postRating(id: Int, ratingBody: RatingBody)
suspend fun deleteRating(id: Int)
suspend fun getKeywords(id: Int)
suspend fun getKeywords(id: Int, refreshing: Boolean)
suspend fun getWatchProviders(id: Int)
suspend fun getWatchProviders(id: Int, refreshing: Boolean)
suspend fun getExternalIds(id: Int)
suspend fun getExternalIds(id: Int, refreshing: Boolean)
suspend fun getAccountStates(id: Int)

View File

@@ -6,6 +6,8 @@ import androidx.compose.runtime.mutableStateOf
import androidx.paging.PagingData
import androidx.paging.PagingSource
import androidx.paging.PagingState
import com.owenlejeune.tvtime.api.LoadingState
import com.owenlejeune.tvtime.api.loadRemoteData
import com.owenlejeune.tvtime.api.storedIn
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.AccountStates
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.CastMember
@@ -58,62 +60,112 @@ class MoviesService: KoinComponent, DetailService, HomePageService {
val accountStates = Collections.synchronizedMap(mutableStateMapOf<Int, AccountStates>())
val keywordResults = Collections.synchronizedMap(mutableStateMapOf<Int, Flow<PagingData<SearchResultMedia>>>())
val detailsLoadingState = mutableStateOf(LoadingState.INACTIVE)
val imagesLoadingState = mutableStateOf(LoadingState.INACTIVE)
val castCrewLoadingState = mutableStateOf(LoadingState.INACTIVE)
val videosLoadingState = mutableStateOf(LoadingState.INACTIVE)
val reviewsLoadingState = mutableStateOf(LoadingState.INACTIVE)
val keywordsLoadingState = mutableStateOf(LoadingState.INACTIVE)
val watchProvidersLoadingState = mutableStateOf(LoadingState.INACTIVE)
val externalIdsLoadingState = mutableStateOf(LoadingState.INACTIVE)
val releaseDatesLoadingState = mutableStateOf(LoadingState.INACTIVE)
val accountStatesLoadingState = mutableStateOf(LoadingState.INACTIVE)
override suspend fun getById(id: Int) {
movieService.getMovieById(id) storedIn { detailMovies[id] = it }
override suspend fun getById(id: Int, refreshing: Boolean) {
loadRemoteData(
{ movieService.getMovieById(id) },
{ detailMovies[id] = it },
detailsLoadingState,
refreshing
)
}
override suspend fun getImages(id: Int) {
movieService.getMovieImages(id) storedIn { images[id] = it }
override suspend fun getImages(id: Int, refreshing: Boolean) {
loadRemoteData(
{ movieService.getMovieImages(id) },
{ images[id] = it },
imagesLoadingState,
refreshing
)
}
override suspend fun getCastAndCrew(id: Int) {
movieService.getCastAndCrew(id) storedIn {
cast[id] = it.cast
crew[id] = it.crew
}
override suspend fun getCastAndCrew(id: Int, refreshing: Boolean) {
loadRemoteData(
{ movieService.getCastAndCrew(id) },
{
cast[id] = it.cast
crew[id] = it.crew
},
castCrewLoadingState,
refreshing
)
}
override suspend fun getVideos(id: Int) {
movieService.getVideos(id) storedIn { videos[id] = it.results }
override suspend fun getVideos(id: Int, refreshing: Boolean) {
loadRemoteData(
{ movieService.getVideos(id) },
{ videos[id] = it.results },
videosLoadingState,
refreshing
)
}
override suspend fun getReviews(id: Int) {
movieService.getReviews(id) storedIn { reviews[id] = it.results }
override suspend fun getReviews(id: Int, refreshing: Boolean) {
loadRemoteData(
{ movieService.getReviews(id) },
{ reviews[id] = it.results },
reviewsLoadingState,
refreshing
)
}
override suspend fun getKeywords(id: Int) {
movieService.getKeywords(id) storedIn { keywords[id] = it.keywords ?: emptyList() }
override suspend fun getKeywords(id: Int, refreshing: Boolean) {
loadRemoteData(
{ movieService.getKeywords(id) },
{ keywords[id] = it.keywords ?: emptyList() },
keywordsLoadingState,
refreshing
)
}
override suspend fun getWatchProviders(id: Int) {
movieService.getWatchProviders(id) storedIn {
it.results[Locale.getDefault().country]?.let { wp ->
watchProviders[id] = wp
}
}
override suspend fun getWatchProviders(id: Int, refreshing: Boolean) {
loadRemoteData(
{ movieService.getWatchProviders(id) },
{
it.results[Locale.getDefault().country]?.let { wp ->
watchProviders[id] = wp
}
},
watchProvidersLoadingState,
refreshing
)
}
override suspend fun getExternalIds(id: Int) {
movieService.getExternalIds(id) storedIn { externalIds[id] = it }
override suspend fun getExternalIds(id: Int, refreshing: Boolean) {
loadRemoteData(
{ movieService.getExternalIds(id) },
{ externalIds[id] = it },
externalIdsLoadingState,
refreshing
)
}
override suspend fun getAccountStates(id: Int) {
val sessionId = SessionManager.currentSession.value?.sessionId ?: throw Exception("Session must not be null")
val response = movieService.getAccountStates(id, sessionId)
if (response.isSuccessful) {
response.body()?.let {
Log.d(TAG, "Successfully got account states: $it")
accountStates[id] = it
} ?: run {
Log.d(TAG, "Problem getting account states")
}
} else {
Log.d(TAG, "Issue getting account states: $response")
}
loadRemoteData(
{ movieService.getAccountStates(id, sessionId) },
{ accountStates[id] = it },
accountStatesLoadingState,
false
)
}
suspend fun getReleaseDates(id: Int) {
movieService.getReleaseDates(id) storedIn { releaseDates[id] = it.releaseDates }
suspend fun getReleaseDates(id: Int, refreshing: Boolean) {
loadRemoteData(
{ movieService.getReleaseDates(id) },
{ releaseDates[id] = it.releaseDates },
releaseDatesLoadingState,
refreshing
)
}
override suspend fun postRating(id: Int, ratingBody: RatingBody) {

View File

@@ -1,7 +1,9 @@
package com.owenlejeune.tvtime.api.tmdb.api.v3
import android.util.Log
import androidx.compose.runtime.mutableStateMapOf
import androidx.compose.runtime.mutableStateOf
import com.owenlejeune.tvtime.api.LoadingState
import com.owenlejeune.tvtime.api.loadRemoteData
import com.owenlejeune.tvtime.api.tmdb.TmdbClient
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.DetailCast
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.DetailCrew
@@ -28,61 +30,48 @@ class PeopleService: KoinComponent {
val imagesMap = Collections.synchronizedMap(mutableStateMapOf<Int, List<PersonImage>>())
val externalIdsMap = Collections.synchronizedMap(mutableStateMapOf<Int, ExternalIds>())
suspend fun getPerson(id: Int) {
val response = service.getPerson(id)
if (response.isSuccessful) {
response.body()?.let {
Log.d(TAG, "Successfully got person $id")
peopleMap[id] = it
} ?: run {
Log.w(TAG, "Problem getting person $id")
}
} else {
Log.e(TAG, "Issue getting person $id")
}
val detailsLoadingState = mutableStateOf(LoadingState.INACTIVE)
val castCrewLoadingState = mutableStateOf(LoadingState.INACTIVE)
val imagesLoadingState = mutableStateOf(LoadingState.INACTIVE)
val externalIdsLoadingState = mutableStateOf(LoadingState.INACTIVE)
suspend fun getPerson(id: Int, refreshing: Boolean) {
loadRemoteData(
{ service.getPerson(id) },
{ peopleMap[id] = it },
detailsLoadingState,
refreshing
)
}
suspend fun getCredits(id: Int) {
val response = service.getCredits(id)
if (response.isSuccessful) {
response.body()?.let {
Log.d(TAG, "Successfully got credits $id")
suspend fun getCredits(id: Int, refreshing: Boolean) {
loadRemoteData(
{ service.getCredits(id) },
{
castMap[id] = it.cast
crewMap[id] = it.crew
} ?: run {
Log.w(TAG, "Problem getting credits $id")
}
} else {
Log.e(TAG, "Issue getting credits $id")
}
},
castCrewLoadingState,
refreshing
)
}
suspend fun getImages(id: Int) {
val response = service.getImages(id)
if (response.isSuccessful) {
response.body()?.let {
Log.d(TAG, "Successfully got images $id")
imagesMap[id] = it.images
} ?: run {
Log.w(TAG, "Problem getting images $id")
}
} else {
Log.e(TAG, "Issues getting images $id")
}
suspend fun getImages(id: Int, refreshing: Boolean) {
loadRemoteData(
{ service.getImages(id) },
{ imagesMap[id] = it.images },
imagesLoadingState,
refreshing
)
}
suspend fun getExternalIds(id: Int) {
val response = service.getExternalIds(id)
if (response.isSuccessful) {
response.body()?.let {
Log.d(TAG, "Successfully got external ids $id")
externalIdsMap[id] = it
} ?: run {
Log.w(TAG, "Problem getting external ids $id")
}
} else {
Log.e(TAG, "Issue getting external ids $id")
}
suspend fun getExternalIds(id: Int, refreshing: Boolean) {
loadRemoteData(
{ service.getExternalIds(id) },
{ externalIdsMap[id] = it },
externalIdsLoadingState,
refreshing
)
}
suspend fun getPopular(page: Int): Response<HomePagePeopleResponse> {

View File

@@ -2,9 +2,12 @@ package com.owenlejeune.tvtime.api.tmdb.api.v3
import android.util.Log
import androidx.compose.runtime.mutableStateMapOf
import androidx.compose.runtime.mutableStateOf
import androidx.paging.PagingData
import androidx.paging.PagingSource
import androidx.paging.PagingState
import com.owenlejeune.tvtime.api.LoadingState
import com.owenlejeune.tvtime.api.loadRemoteData
import com.owenlejeune.tvtime.api.storedIn
import com.owenlejeune.tvtime.api.tmdb.TmdbClient
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.AccountStates
@@ -66,59 +69,126 @@ class TvService: KoinComponent, DetailService, HomePageService {
val seasons: MutableMap<Int, out Set<Season>>
get() = _seasons
override suspend fun getById(id: Int) {
service.getTvShowById(id) storedIn { detailTv[id] = it }
val detailsLoadingState = mutableStateOf(LoadingState.INACTIVE)
val imagesLoadingState = mutableStateOf(LoadingState.INACTIVE)
val castCrewLoadingState = mutableStateOf(LoadingState.INACTIVE)
val contentRatingsLoadingState = mutableStateOf(LoadingState.INACTIVE)
val videosLoadingState = mutableStateOf(LoadingState.INACTIVE)
val reviewsLoadingState = mutableStateOf(LoadingState.INACTIVE)
val keywordsLoadingState = mutableStateOf(LoadingState.INACTIVE)
val watchProvidersLoadingState = mutableStateOf(LoadingState.INACTIVE)
val externalIdsLoadingState = mutableStateOf(LoadingState.INACTIVE)
val accountStatesLoadingState = mutableStateOf(LoadingState.INACTIVE)
val seasonsLoadingState = mutableStateOf(LoadingState.INACTIVE)
override suspend fun getById(id: Int, refreshing: Boolean) {
loadRemoteData(
{ service.getTvShowById(id) },
{ detailTv[id] = it },
detailsLoadingState,
refreshing
)
}
override suspend fun getImages(id: Int) {
service.getTvImages(id) storedIn { images[id] = it }
override suspend fun getImages(id: Int, refreshing: Boolean) {
loadRemoteData(
{ service.getTvImages(id) },
{ images[id] = it },
imagesLoadingState,
refreshing
)
}
override suspend fun getCastAndCrew(id: Int) {
service.getCastAndCrew(id) storedIn {
cast[id] = it.cast
crew[id] = it.crew
}
override suspend fun getCastAndCrew(id: Int, refreshing: Boolean) {
loadRemoteData(
{ service.getCastAndCrew(id) },
{
cast[id] = it.cast
crew[id] = it.crew
},
castCrewLoadingState,
refreshing
)
}
suspend fun getContentRatings(id: Int) {
service.getContentRatings(id) storedIn { contentRatings[id] = it.results }
suspend fun getContentRatings(id: Int, refreshing: Boolean) {
loadRemoteData(
{ service.getContentRatings(id) },
{ contentRatings[id] = it.results },
contentRatingsLoadingState,
refreshing
)
}
override suspend fun getVideos(id: Int) {
service.getVideos(id) storedIn { videos[id] = it.results }
override suspend fun getVideos(id: Int, refreshing: Boolean) {
loadRemoteData(
{ service.getVideos(id) },
{ videos[id] = it.results },
videosLoadingState,
refreshing
)
}
override suspend fun getReviews(id: Int) {
service.getReviews(id) storedIn { reviews[id] = it.results }
override suspend fun getReviews(id: Int, refreshing: Boolean) {
loadRemoteData(
{ service.getReviews(id) },
{ reviews[id] = it.results },
reviewsLoadingState,
refreshing
)
}
override suspend fun getKeywords(id: Int) {
service.getKeywords(id) storedIn { keywords[id] = it.keywords ?: emptyList() }
override suspend fun getKeywords(id: Int, refreshing: Boolean) {
loadRemoteData(
{ service.getKeywords(id) },
{ keywords[id] = it.keywords ?: emptyList() },
keywordsLoadingState,
refreshing
)
}
override suspend fun getWatchProviders(id: Int) {
service.getWatchProviders(id) storedIn {
it.results[Locale.getDefault().country]?.let { wp ->
watchProviders[id] = wp
}
}
override suspend fun getWatchProviders(id: Int, refreshing: Boolean) {
loadRemoteData(
{ service.getWatchProviders(id) },
{
it.results[Locale.getDefault().country]?.let { wp ->
watchProviders[id] = wp
}
},
watchProvidersLoadingState,
refreshing
)
}
override suspend fun getAccountStates(id: Int) {
service.getAccountStates(id) storedIn { accountStates[id] = it }
loadRemoteData(
{ service.getAccountStates(id) },
{ accountStates[id] = it },
accountStatesLoadingState,
false
)
}
suspend fun getSeason(seriesId: Int, seasonId: Int) {
service.getSeason(seriesId, seasonId) storedIn {
_seasons[seriesId]?.add(it) ?: run {
_seasons[seriesId] = mutableSetOf(it)
}
}
suspend fun getSeason(seriesId: Int, seasonId: Int, refreshing: Boolean) {
loadRemoteData(
{ service.getSeason(seriesId, seasonId) },
{
_seasons[seriesId]?.add(it) ?: run {
_seasons[seriesId] = mutableSetOf(it)
}
},
seasonsLoadingState,
refreshing
)
}
override suspend fun getExternalIds(id: Int) {
service.getExternalIds(id) storedIn { externalIds[id] = it }
override suspend fun getExternalIds(id: Int, refreshing: Boolean) {
loadRemoteData(
{ service.getExternalIds(id) },
{ externalIds[id] = it },
externalIdsLoadingState,
refreshing
)
}
override suspend fun postRating(id: Int, ratingBody: RatingBody) {

View File

@@ -6,4 +6,6 @@ import kotlinx.coroutines.launch
fun Any.coroutineTask(runnable: suspend () -> Unit) {
CoroutineScope(Dispatchers.IO).launch { runnable() }
}
}
fun <T> anyOf(vararg items: T, predicate: (T) -> Boolean): Boolean = items.any(predicate)

View File

@@ -19,6 +19,7 @@ import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.dp
@@ -252,11 +253,13 @@ private fun ExternalIdLogo(
context.startActivity(intent)
},
modifier = Modifier.size(28.dp)
// modifier = Modifier.size(40.dp)
) {
Icon(
painter = logoPainter,
contentDescription = null,
tint = MaterialTheme.colorScheme.secondary
tint = MaterialTheme.colorScheme.primary,
modifier = Modifier.size(28.dp)
)
}
}

View File

@@ -3,17 +3,56 @@ package com.owenlejeune.tvtime.ui.screens
import android.content.Intent
import android.net.Uri
import android.widget.Toast
import androidx.compose.animation.*
import androidx.compose.foundation.*
import androidx.compose.foundation.layout.*
import androidx.compose.animation.Crossfade
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.horizontalScroll
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material.icons.filled.Movie
import androidx.compose.material.icons.filled.Send
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.material.pullrefresh.PullRefreshIndicator
import androidx.compose.material.pullrefresh.pullRefresh
import androidx.compose.material.pullrefresh.rememberPullRefreshState
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.Divider
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.rememberTopAppBarState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
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
import androidx.compose.ui.draw.clip
@@ -39,7 +78,18 @@ import com.google.accompanist.pager.PagerState
import com.google.accompanist.pager.rememberPagerState
import com.google.accompanist.systemuicontroller.rememberSystemUiController
import com.owenlejeune.tvtime.R
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.*
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
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.Genre
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.ImageCollection
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.MovieCastMember
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.MovieCrewMember
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.Person
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.TvCastMember
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.TvCrewMember
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.Video
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.WatchProviderDetails
import com.owenlejeune.tvtime.extensions.DateFormat
import com.owenlejeune.tvtime.extensions.WindowSizeClass
import com.owenlejeune.tvtime.extensions.format
@@ -47,17 +97,63 @@ import com.owenlejeune.tvtime.extensions.getCalendarYear
import com.owenlejeune.tvtime.extensions.lazyPagingItems
import com.owenlejeune.tvtime.extensions.listItems
import com.owenlejeune.tvtime.preferences.AppPreferences
import com.owenlejeune.tvtime.ui.components.*
import com.owenlejeune.tvtime.ui.components.ActionsView
import com.owenlejeune.tvtime.ui.components.AvatarImage
import com.owenlejeune.tvtime.ui.components.ChipDefaults
import com.owenlejeune.tvtime.ui.components.ChipGroup
import com.owenlejeune.tvtime.ui.components.ChipInfo
import com.owenlejeune.tvtime.ui.components.ChipStyle
import com.owenlejeune.tvtime.ui.components.CircleBackgroundColorImage
import com.owenlejeune.tvtime.ui.components.ContentCard
import com.owenlejeune.tvtime.ui.components.DetailHeader
import com.owenlejeune.tvtime.ui.components.ExpandableContentCard
import com.owenlejeune.tvtime.ui.components.ExternalIdsArea
import com.owenlejeune.tvtime.ui.components.FullScreenThumbnailVideoPlayer
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.PosterItem
import com.owenlejeune.tvtime.ui.components.RoundedChip
import com.owenlejeune.tvtime.ui.components.RoundedTextField
import com.owenlejeune.tvtime.ui.components.SelectableTextChip
import com.owenlejeune.tvtime.ui.components.TwoLineImageTextCard
import com.owenlejeune.tvtime.ui.navigation.AppNavItem
import com.owenlejeune.tvtime.ui.viewmodel.MainViewModel
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.*
import kotlinx.coroutines.launch
import org.koin.java.KoinJavaComponent.get
@OptIn(ExperimentalMaterial3Api::class, ExperimentalPagerApi::class)
private suspend 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)
when (type) {
MediaViewType.MOVIE -> {
mainViewModel.getReleaseDates(itemId, force)
}
MediaViewType.TV -> {
mainViewModel.getContentRatings(itemId, force)
}
else -> {}
}
}
@OptIn(ExperimentalMaterial3Api::class, ExperimentalPagerApi::class, ExperimentalMaterialApi::class)
@Composable
fun MediaDetailScreen(
appNavController: NavController,
@@ -65,17 +161,14 @@ fun MediaDetailScreen(
type: MediaViewType,
windowSize: WindowSizeClass
) {
val scope = rememberCoroutineScope()
val mainViewModel = viewModel<MainViewModel>()
val systemUiController = rememberSystemUiController()
systemUiController.setStatusBarColor(color = MaterialTheme.colorScheme.background)
systemUiController.setNavigationBarColor(color = MaterialTheme.colorScheme.background)
LaunchedEffect(Unit) {
mainViewModel.getById(itemId, type)
mainViewModel.getImages(itemId, type)
}
val mediaItems: Map<Int, DetailedItem> = remember { mainViewModel.produceDetailsFor(type) }
val mediaItem = mediaItems[itemId]
@@ -87,6 +180,30 @@ fun MediaDetailScreen(
val pagerState = rememberPagerState(initialPage = 0)
LaunchedEffect(Unit) {
fetchData(mainViewModel, itemId, type)
}
if (type == MediaViewType.TV) {
LaunchedEffect(mediaItem) {
val lastSeason = (mediaItem as DetailedTv?)?.numberOfSeasons ?: 0
if (lastSeason > 0) {
mainViewModel.getSeason(itemId, lastSeason)
}
}
}
val isRefreshing = remember { mutableStateOf(false) }
mainViewModel.monitorDetailsLoadingRefreshing(refreshing = isRefreshing)
val pullRefreshState = rememberPullRefreshState(
refreshing = isRefreshing.value,
onRefresh = {
scope.launch {
fetchData(mainViewModel, itemId, type, true)
}
}
)
Box(
modifier = Modifier.fillMaxSize()
) {
@@ -116,7 +233,10 @@ fun MediaDetailScreen(
)
}
) { innerPadding ->
Box(modifier = Modifier.padding(innerPadding)) {
Box(modifier = Modifier
.padding(innerPadding)
.pullRefresh(state = pullRefreshState)
) {
MediaViewContent(
appNavController = appNavController,
itemId = itemId,
@@ -128,6 +248,11 @@ fun MediaDetailScreen(
pagerState = pagerState,
mainViewModel = mainViewModel
)
PullRefreshIndicator(
refreshing = isRefreshing.value,
state = pullRefreshState,
modifier = Modifier.align(alignment = Alignment.TopCenter)
)
}
}
@@ -143,7 +268,7 @@ fun MediaDetailScreen(
}
}
@OptIn(ExperimentalPagerApi::class)
@OptIn(ExperimentalPagerApi::class, ExperimentalMaterialApi::class)
@Composable
private fun MediaViewContent(
appNavController: NavController,
@@ -157,10 +282,6 @@ private fun MediaViewContent(
pagerState: PagerState,
preferences: AppPreferences = get(AppPreferences::class.java)
) {
LaunchedEffect(Unit) {
mainViewModel.getExternalIds(itemId, type)
}
Row(
modifier = Modifier
.background(color = MaterialTheme.colorScheme.background),
@@ -290,9 +411,6 @@ private fun MiscTvDetails(
mainViewModel: MainViewModel
) {
mediaItem?.let { tv ->
LaunchedEffect(Unit) {
mainViewModel.getContentRatings(itemId)
}
val series = tv as DetailedTv
val contentRatingsMap = remember { mainViewModel.tvContentRatings }
@@ -318,10 +436,6 @@ private fun MiscMovieDetails(
mainViewModel: MainViewModel
) {
mediaItem?.let { mi ->
LaunchedEffect(Unit) {
mainViewModel.getReleaseDates(itemId)
}
val movie = mi as DetailedMovie
val contentRatingsMap = remember { mainViewModel.movieReleaseDates }
@@ -388,10 +502,6 @@ private fun OverviewCard(
appNavController: NavController,
modifier: Modifier = Modifier
) {
LaunchedEffect(Unit) {
mainViewModel.getKeywords(itemId, type)
}
val keywordsMap = remember { mainViewModel.produceKeywordsFor(type) }
val keywords = keywordsMap[itemId]
@@ -602,10 +712,6 @@ private fun CastCard(
appNavController: NavController,
modifier: Modifier = Modifier
) {
LaunchedEffect(Unit) {
mainViewModel.getCastAndCrew(itemId, type)
}
val castMap = remember { mainViewModel.produceCastFor(type) }
val cast = castMap[itemId]
@@ -694,13 +800,6 @@ private fun SeasonCard(
mainViewModel: MainViewModel,
appNavController: NavController
) {
LaunchedEffect(mediaItem) {
val lastSeason = (mediaItem as DetailedTv?)?.numberOfSeasons ?: 0
if (lastSeason > 0) {
mainViewModel.getSeason(itemId, lastSeason)
}
}
val seasonsMap = remember { mainViewModel.tvSeasons }
val lastSeason = seasonsMap[itemId]?.lastOrNull()
@@ -766,10 +865,6 @@ fun SimilarContentCard(
appNavController: NavController,
modifier: Modifier = Modifier
) {
LaunchedEffect(Unit) {
mainViewModel.getSimilar(itemId, mediaType)
}
val similarContentMap = remember { mainViewModel.produceSimilarContentFor(mediaType) }
val similarContent = similarContentMap[itemId]
val pagingItems = similarContent?.collectAsLazyPagingItems()
@@ -822,10 +917,6 @@ fun VideosCard(
mainViewModel: MainViewModel,
modifier: Modifier = Modifier
) {
LaunchedEffect(Unit) {
mainViewModel.getVideos(itemId, type)
}
val videosMap = remember { mainViewModel.produceVideosFor(type) }
val videos = videosMap[itemId]
@@ -902,10 +993,6 @@ private fun WatchProvidersCard(
mainViewModel: MainViewModel,
modifier: Modifier = Modifier
) {
LaunchedEffect(Unit) {
mainViewModel.getWatchProviders(itemId, type)
}
val watchProvidersMap = remember { mainViewModel.produceWatchProvidersFor(type) }
val watchProviders = watchProvidersMap[itemId]
watchProviders?.let { providers ->
@@ -1116,10 +1203,6 @@ private fun ReviewsCard(
mainViewModel: MainViewModel,
modifier: Modifier = Modifier
) {
LaunchedEffect(Unit) {
mainViewModel.getReviews(itemId, type)
}
val reviewsMap = remember { mainViewModel.produceReviewsFor(type) }
val reviews = reviewsMap[itemId]

View File

@@ -1,5 +1,6 @@
package com.owenlejeune.tvtime.ui.screens
import android.graphics.Paint.Align
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
@@ -14,9 +15,13 @@ import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material.icons.filled.Person
import androidx.compose.material.pullrefresh.PullRefreshIndicator
import androidx.compose.material.pullrefresh.pullRefresh
import androidx.compose.material.pullrefresh.rememberPullRefreshState
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
@@ -28,7 +33,10 @@ import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.rememberTopAppBarState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.res.stringResource
@@ -51,22 +59,33 @@ import com.owenlejeune.tvtime.ui.navigation.AppNavItem
import com.owenlejeune.tvtime.ui.viewmodel.MainViewModel
import com.owenlejeune.tvtime.utils.TmdbUtils
import com.owenlejeune.tvtime.utils.types.MediaViewType
import kotlinx.coroutines.launch
import java.lang.Integer.min
private const val TAG = "PeopleDetailScreen"
@OptIn(ExperimentalMaterial3Api::class, ExperimentalPagerApi::class)
private suspend fun fetchData(
mainViewModel: MainViewModel,
id: Int,
force: Boolean = false
) {
mainViewModel.getById(id, MediaViewType.PERSON, force)
mainViewModel.getExternalIds(id, MediaViewType.PERSON, force)
mainViewModel.getCastAndCrew(id, MediaViewType.PERSON, force)
mainViewModel.getImages(id, MediaViewType.PERSON, force)
}
@OptIn(ExperimentalMaterial3Api::class, ExperimentalPagerApi::class, ExperimentalMaterialApi::class)
@Composable
fun PersonDetailScreen(
appNavController: NavController,
personId: Int
) {
val scope = rememberCoroutineScope()
val mainViewModel = viewModel<MainViewModel>()
LaunchedEffect(Unit) {
mainViewModel.getById(personId, MediaViewType.PERSON)
mainViewModel.getExternalIds(personId, MediaViewType.PERSON)
mainViewModel.getCastAndCrew(personId, MediaViewType.PERSON)
mainViewModel.getImages(personId, MediaViewType.PERSON)
fetchData(mainViewModel, personId)
}
val systemUiController = rememberSystemUiController()
@@ -79,6 +98,17 @@ fun PersonDetailScreen(
val topAppBarScrollState = rememberTopAppBarState()
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(topAppBarScrollState)
val isRefreshing = remember { mutableStateOf(false) }
mainViewModel.monitorDetailsLoadingRefreshing(refreshing = isRefreshing)
val pullRefreshState = rememberPullRefreshState(
refreshing = isRefreshing.value,
onRefresh = {
scope.launch {
fetchData(mainViewModel, personId, true)
}
}
)
Scaffold(
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
topBar = {
@@ -102,7 +132,10 @@ fun PersonDetailScreen(
)
}
) { innerPadding ->
Box(modifier = Modifier.padding(innerPadding)) {
Box(modifier = Modifier
.padding(innerPadding)
.pullRefresh(state = pullRefreshState)
) {
Column(
modifier = Modifier
.background(color = MaterialTheme.colorScheme.background)
@@ -131,6 +164,12 @@ fun PersonDetailScreen(
ImagesCard(id = personId, appNavController = appNavController)
}
PullRefreshIndicator(
refreshing = isRefreshing.value,
state = pullRefreshState,
modifier = Modifier.align(alignment = Alignment.TopCenter)
)
}
}
}
@@ -257,7 +296,12 @@ private fun ImagesCard(
modifier = Modifier
.padding(start = 16.dp, bottom = 16.dp)
.clickable {
appNavController.navigate(AppNavItem.GalleryView.withArgs(MediaViewType.PERSON, id))
appNavController.navigate(
AppNavItem.GalleryView.withArgs(
MediaViewType.PERSON,
id
)
)
}
)
}

View File

@@ -399,7 +399,7 @@ private fun MovieSearchResultView(
service: MoviesService = get(MoviesService::class.java)
) {
LaunchedEffect(Unit) {
service.getCastAndCrew(result.id)
service.getCastAndCrew(result.id, false)
}
val mainViewModel = viewModel<MainViewModel>()
val castMap = remember { mainViewModel.movieCast }
@@ -428,7 +428,7 @@ private fun TvSearchResultView(
) {
val context = LocalContext.current
LaunchedEffect(Unit) {
service.getCastAndCrew(result.id)
service.getCastAndCrew(result.id, false)
}
val mainViewModel = viewModel<MainViewModel>()
val castMap = remember { mainViewModel.tvCast }

View File

@@ -1,7 +1,12 @@
package com.owenlejeune.tvtime.ui.viewmodel
import android.annotation.SuppressLint
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.remember
import androidx.lifecycle.ViewModel
import androidx.paging.PagingData
import com.owenlejeune.tvtime.api.LoadingState
import com.owenlejeune.tvtime.api.tmdb.api.createPagingFlow
import com.owenlejeune.tvtime.api.tmdb.api.v3.MoviesService
import com.owenlejeune.tvtime.api.tmdb.api.v3.PeopleService
@@ -19,6 +24,7 @@ import com.owenlejeune.tvtime.api.tmdb.api.v3.model.SearchResultMedia
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.TmdbItem
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.Video
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.WatchProviders
import com.owenlejeune.tvtime.extensions.anyOf
import com.owenlejeune.tvtime.ui.screens.tabs.MediaTabNavItem
import com.owenlejeune.tvtime.utils.types.MediaViewType
import com.owenlejeune.tvtime.utils.types.TimeWindow
@@ -47,6 +53,17 @@ class MainViewModel: ViewModel(), KoinComponent {
val movieAccountStates = movieService.accountStates
val movieKeywordResults = movieService.keywordResults
val movieDetailsLoadingState = movieService.detailsLoadingState
val movieImagesLoadingState = movieService.imagesLoadingState
val movieCastCrewLoadingState = movieService.castCrewLoadingState
val movieVideosLoadingState = movieService.videosLoadingState
val movieReviewsLoadingState = movieService.reviewsLoadingState
val movieKeywordsLoadingState = movieService.keywordsLoadingState
val movieWatchProvidersLoadingState = movieService.watchProvidersLoadingState
val movieExternalIdsLoadingState = movieService.externalIdsLoadingState
val movieReleaseDatesLoadingState = movieService.releaseDatesLoadingState
val movieAccountStatesLoadingState = movieService.accountStatesLoadingState
val popularMovies by lazy {
createPagingFlow(
fetcher = { p -> movieService.getPopular(p) },
@@ -87,6 +104,18 @@ class MainViewModel: ViewModel(), KoinComponent {
val tvAccountStates = tvService.accountStates
val tvKeywordResults = tvService.keywordResults
val tvDetailsLoadingState = tvService.detailsLoadingState
val tvImagesLoadingState = tvService.imagesLoadingState
val tvCastCrewLoadingState = tvService.castCrewLoadingState
val tvVideosLoadingState = tvService.videosLoadingState
val tvReviewsLoadingState = tvService.reviewsLoadingState
val tvKeywordsLoadingState = tvService.keywordsLoadingState
val tvWatchProvidersLoadingState = tvService.watchProvidersLoadingState
val tvExternalIdsLoadingState = tvService.externalIdsLoadingState
val tvSeasonsLoadingState = tvService.seasonsLoadingState
val tvContentRatingsLoadingState = tvService.contentRatingsLoadingState
val tvAccountStatesLoadingState = tvService.accountStatesLoadingState
val popularTv by lazy {
createPagingFlow(
fetcher = { p -> tvService.getPopular(p) },
@@ -118,6 +147,11 @@ class MainViewModel: ViewModel(), KoinComponent {
val peopleImagesMap = peopleService.imagesMap
val peopleExternalIdsMap = peopleService.externalIdsMap
val peopleDetailsLoadingState = peopleService.detailsLoadingState
val peopleCastCrewLoadingState = peopleService.castCrewLoadingState
val peopleImagesLoadingState = peopleService.imagesLoadingState
val peopleExternalIdsLoadingState = peopleService.externalIdsLoadingState
val popularPeople by lazy {
createPagingFlow(
fetcher = { p -> peopleService.getPopular(p) },
@@ -231,68 +265,68 @@ class MainViewModel: ViewModel(), KoinComponent {
suspend fun getById(id: Int, type: MediaViewType, force: Boolean = false) {
when (type) {
MediaViewType.MOVIE -> if (detailMovies[id] == null || force) movieService.getById(id)
MediaViewType.TV -> if (detailedTv[id] == null || force) tvService.getById(id)
MediaViewType.PERSON -> if (peopleMap[id] == null || force) peopleService.getPerson(id)
MediaViewType.MOVIE -> if (detailMovies[id] == null || force) movieService.getById(id, force)
MediaViewType.TV -> if (detailedTv[id] == null || force) tvService.getById(id, force)
MediaViewType.PERSON -> if (peopleMap[id] == null || force) peopleService.getPerson(id, force)
else -> {}
}
}
suspend fun getImages(id: Int, type: MediaViewType, force: Boolean = false) {
when (type) {
MediaViewType.MOVIE -> if (movieImages[id] == null || force) movieService.getImages(id)
MediaViewType.TV -> if (tvImages[id] == null || force) tvService.getImages(id)
MediaViewType.PERSON -> if (peopleImagesMap[id] == null || force) peopleService.getImages(id)
MediaViewType.MOVIE -> if (movieImages[id] == null || force) movieService.getImages(id, force)
MediaViewType.TV -> if (tvImages[id] == null || force) tvService.getImages(id, force)
MediaViewType.PERSON -> if (peopleImagesMap[id] == null || force) peopleService.getImages(id, force)
else -> {}
}
}
suspend fun getCastAndCrew(id: Int, type: MediaViewType, force: Boolean = false) {
when (type) {
MediaViewType.MOVIE -> if (movieCast[id] == null || movieCrew[id] == null || force) movieService.getCastAndCrew(id)
MediaViewType.TV -> if (tvCast[id] == null || tvCrew[id] == null || force) tvService.getCastAndCrew(id)
MediaViewType.PERSON -> if (peopleCastMap[id] == null || peopleCrewMap[id] == null || force) peopleService.getCredits(id)
MediaViewType.MOVIE -> if (movieCast[id] == null || movieCrew[id] == null || force) movieService.getCastAndCrew(id, force)
MediaViewType.TV -> if (tvCast[id] == null || tvCrew[id] == null || force) tvService.getCastAndCrew(id, force)
MediaViewType.PERSON -> if (peopleCastMap[id] == null || peopleCrewMap[id] == null || force) peopleService.getCredits(id, force)
else -> {}
}
}
suspend fun getVideos(id: Int, type: MediaViewType, force: Boolean = false) {
when (type) {
MediaViewType.MOVIE -> if (movieVideos[id] == null || force) movieService.getVideos(id)
MediaViewType.TV -> if (tvVideos[id] == null || force) tvService.getVideos(id)
MediaViewType.MOVIE -> if (movieVideos[id] == null || force) movieService.getVideos(id, force)
MediaViewType.TV -> if (tvVideos[id] == null || force) tvService.getVideos(id, force)
else -> {}
}
}
suspend fun getReviews(id: Int, type: MediaViewType, force: Boolean = false) {
when (type) {
MediaViewType.MOVIE -> if (movieReviews[id] == null || force) movieService.getReviews(id)
MediaViewType.TV -> if (tvReviews[id] == null || force) tvService.getReviews(id)
MediaViewType.MOVIE -> if (movieReviews[id] == null || force) movieService.getReviews(id, force)
MediaViewType.TV -> if (tvReviews[id] == null || force) tvService.getReviews(id, force)
else -> {}
}
}
suspend fun getKeywords(id: Int, type: MediaViewType, force: Boolean = false) {
when (type) {
MediaViewType.MOVIE -> if (movieKeywords[id] == null || force) movieService.getKeywords(id)
MediaViewType.TV -> if (tvKeywords[id] == null || force) tvService.getKeywords(id)
MediaViewType.MOVIE -> if (movieKeywords[id] == null || force) movieService.getKeywords(id, force)
MediaViewType.TV -> if (tvKeywords[id] == null || force) tvService.getKeywords(id, force)
else -> {}
}
}
suspend fun getWatchProviders(id: Int, type: MediaViewType, force: Boolean = false) {
when (type) {
MediaViewType.MOVIE -> if (movieWatchProviders[id] == null || force) movieService.getWatchProviders(id)
MediaViewType.TV -> if (tvWatchProviders[id] == null || force) tvService.getWatchProviders(id)
MediaViewType.MOVIE -> if (movieWatchProviders[id] == null || force) movieService.getWatchProviders(id, force)
MediaViewType.TV -> if (tvWatchProviders[id] == null || force) tvService.getWatchProviders(id, force)
else -> {}
}
}
suspend fun getExternalIds(id: Int, type: MediaViewType, force: Boolean = false) {
when (type) {
MediaViewType.MOVIE -> if (movieExternalIds[id] == null || force) movieService.getExternalIds(id)
MediaViewType.TV -> if (tvExternalIds[id] == null || force) tvService.getExternalIds(id)
MediaViewType.PERSON -> if (peopleExternalIdsMap[id] == null || force) peopleService.getExternalIds(id)
MediaViewType.MOVIE -> if (movieExternalIds[id] == null || force) movieService.getExternalIds(id, force)
MediaViewType.TV -> if (tvExternalIds[id] == null || force) tvService.getExternalIds(id, force)
MediaViewType.PERSON -> if (peopleExternalIdsMap[id] == null || force) peopleService.getExternalIds(id, force)
else -> {}
}
}
@@ -359,20 +393,64 @@ class MainViewModel: ViewModel(), KoinComponent {
suspend fun getReleaseDates(id: Int, force: Boolean = false) {
if (movieReleaseDates[id] == null || force) {
movieService.getReleaseDates(id)
movieService.getReleaseDates(id, force)
}
}
suspend fun getContentRatings(id: Int, force: Boolean = false) {
if (tvContentRatings[id] == null || force) {
tvService.getContentRatings(id)
tvService.getContentRatings(id, force)
}
}
suspend fun getSeason(seriesId: Int, seasonId: Int, force: Boolean = false) {
if (tvSeasons[seriesId] == null || force) {
tvService.getSeason(seriesId, seasonId)
tvService.getSeason(seriesId, seasonId, force)
}
}
@SuppressLint("ComposableNaming")
@Composable
fun monitorDetailsLoadingRefreshing(refreshing: 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 }
refreshing.value = movieDetails.value == LoadingState.REFRESHING || movieImages.value == LoadingState.REFRESHING ||
movieCastCrew.value == LoadingState.REFRESHING || movieVideos.value == LoadingState.REFRESHING ||
movieReviews.value == LoadingState.REFRESHING || movieKeywords.value == LoadingState.REFRESHING ||
movieWatchProviders.value == LoadingState.REFRESHING || movieExternalIds.value == LoadingState.REFRESHING ||
movieReleaseDates.value == LoadingState.REFRESHING || movieAccountStates.value == LoadingState.REFRESHING ||
tvDetails.value == LoadingState.REFRESHING || tvImages.value == LoadingState.REFRESHING ||
tvCastCrew.value == LoadingState.REFRESHING || tvVideos.value == LoadingState.REFRESHING ||
tvReviews.value == LoadingState.REFRESHING || tvKeywords.value == LoadingState.REFRESHING ||
tvWatchProviders.value == LoadingState.REFRESHING || tvExternalIds.value == LoadingState.REFRESHING ||
tvSeasons.value == LoadingState.REFRESHING || tvContentRatings.value == LoadingState.REFRESHING ||
tvAccountStates.value == LoadingState.REFRESHING || peopleDetails.value == LoadingState.REFRESHING ||
peopleCastCrew.value == LoadingState.REFRESHING || peopleImages.value == LoadingState.REFRESHING ||
peopleExternalIds.value == LoadingState.REFRESHING
}
}