From 2bdff383b7b317529eb94776b1d48277e8a8e78a Mon Sep 17 00:00:00 2001 From: Owen LeJeune Date: Sun, 9 Jul 2023 23:44:04 -0400 Subject: [PATCH] add pull to refresh to details screens --- .../owenlejeune/tvtime/api/LoadingState.kt | 1 + .../owenlejeune/tvtime/api/ServiceUtils.kt | 21 ++ .../tvtime/api/tmdb/api/v3/DetailService.kt | 16 +- .../tvtime/api/tmdb/api/v3/MoviesService.kt | 124 +++++++---- .../tvtime/api/tmdb/api/v3/PeopleService.kt | 85 ++++---- .../tvtime/api/tmdb/api/v3/TvService.kt | 134 +++++++++--- .../tvtime/extensions/AnyExtensions.kt | 4 +- .../tvtime/ui/components/DetailViewCommon.kt | 5 +- .../tvtime/ui/screens/MediaDetailScreen.kt | 199 +++++++++++++----- .../tvtime/ui/screens/PeopleDetailScreen.kt | 58 ++++- .../tvtime/ui/screens/SearchScreen.kt | 4 +- .../tvtime/ui/viewmodel/MainViewModel.kt | 124 +++++++++-- 12 files changed, 559 insertions(+), 216 deletions(-) diff --git a/app/src/main/java/com/owenlejeune/tvtime/api/LoadingState.kt b/app/src/main/java/com/owenlejeune/tvtime/api/LoadingState.kt index d621da1..4e0d583 100644 --- a/app/src/main/java/com/owenlejeune/tvtime/api/LoadingState.kt +++ b/app/src/main/java/com/owenlejeune/tvtime/api/LoadingState.kt @@ -4,6 +4,7 @@ enum class LoadingState { INACTIVE, LOADING, + REFRESHING, COMPLETE, ERROR diff --git a/app/src/main/java/com/owenlejeune/tvtime/api/ServiceUtils.kt b/app/src/main/java/com/owenlejeune/tvtime/api/ServiceUtils.kt index 5785ad0..63849a4 100644 --- a/app/src/main/java/com/owenlejeune/tvtime/api/ServiceUtils.kt +++ b/app/src/main/java/com/owenlejeune/tvtime/api/ServiceUtils.kt @@ -1,5 +1,6 @@ package com.owenlejeune.tvtime.api +import androidx.compose.runtime.MutableState import retrofit2.Response infix fun Response.storedIn(body: (T) -> Unit) { @@ -8,4 +9,24 @@ infix fun Response.storedIn(body: (T) -> Unit) { body(it) } } +} + +suspend fun loadRemoteData( + fetcher: suspend () -> Response, + processor: (T) -> Unit, + loadSate: MutableState, + 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 + } } \ No newline at end of file diff --git a/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/api/v3/DetailService.kt b/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/api/v3/DetailService.kt index 0ee41c5..42bc4c2 100644 --- a/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/api/v3/DetailService.kt +++ b/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/api/v3/DetailService.kt @@ -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 - 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) diff --git a/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/api/v3/MoviesService.kt b/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/api/v3/MoviesService.kt index 6cacc8e..bce93f5 100644 --- a/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/api/v3/MoviesService.kt +++ b/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/api/v3/MoviesService.kt @@ -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()) val keywordResults = Collections.synchronizedMap(mutableStateMapOf>>()) + 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) { diff --git a/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/api/v3/PeopleService.kt b/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/api/v3/PeopleService.kt index 638e392..7ac8768 100644 --- a/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/api/v3/PeopleService.kt +++ b/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/api/v3/PeopleService.kt @@ -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>()) val externalIdsMap = Collections.synchronizedMap(mutableStateMapOf()) - 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 { diff --git a/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/api/v3/TvService.kt b/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/api/v3/TvService.kt index 76f9425..42ebf8d 100644 --- a/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/api/v3/TvService.kt +++ b/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/api/v3/TvService.kt @@ -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> 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) { diff --git a/app/src/main/java/com/owenlejeune/tvtime/extensions/AnyExtensions.kt b/app/src/main/java/com/owenlejeune/tvtime/extensions/AnyExtensions.kt index b95d210..9e65b5a 100644 --- a/app/src/main/java/com/owenlejeune/tvtime/extensions/AnyExtensions.kt +++ b/app/src/main/java/com/owenlejeune/tvtime/extensions/AnyExtensions.kt @@ -6,4 +6,6 @@ import kotlinx.coroutines.launch fun Any.coroutineTask(runnable: suspend () -> Unit) { CoroutineScope(Dispatchers.IO).launch { runnable() } -} \ No newline at end of file +} + +fun anyOf(vararg items: T, predicate: (T) -> Boolean): Boolean = items.any(predicate) \ No newline at end of file diff --git a/app/src/main/java/com/owenlejeune/tvtime/ui/components/DetailViewCommon.kt b/app/src/main/java/com/owenlejeune/tvtime/ui/components/DetailViewCommon.kt index de95b2e..e3088e9 100644 --- a/app/src/main/java/com/owenlejeune/tvtime/ui/components/DetailViewCommon.kt +++ b/app/src/main/java/com/owenlejeune/tvtime/ui/components/DetailViewCommon.kt @@ -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) ) } } \ No newline at end of file diff --git a/app/src/main/java/com/owenlejeune/tvtime/ui/screens/MediaDetailScreen.kt b/app/src/main/java/com/owenlejeune/tvtime/ui/screens/MediaDetailScreen.kt index e4f9f26..95322c0 100644 --- a/app/src/main/java/com/owenlejeune/tvtime/ui/screens/MediaDetailScreen.kt +++ b/app/src/main/java/com/owenlejeune/tvtime/ui/screens/MediaDetailScreen.kt @@ -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() 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 = 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] diff --git a/app/src/main/java/com/owenlejeune/tvtime/ui/screens/PeopleDetailScreen.kt b/app/src/main/java/com/owenlejeune/tvtime/ui/screens/PeopleDetailScreen.kt index 1b56f10..168de0b 100644 --- a/app/src/main/java/com/owenlejeune/tvtime/ui/screens/PeopleDetailScreen.kt +++ b/app/src/main/java/com/owenlejeune/tvtime/ui/screens/PeopleDetailScreen.kt @@ -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() 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 + ) + ) } ) } diff --git a/app/src/main/java/com/owenlejeune/tvtime/ui/screens/SearchScreen.kt b/app/src/main/java/com/owenlejeune/tvtime/ui/screens/SearchScreen.kt index bf6977a..c7b39cd 100644 --- a/app/src/main/java/com/owenlejeune/tvtime/ui/screens/SearchScreen.kt +++ b/app/src/main/java/com/owenlejeune/tvtime/ui/screens/SearchScreen.kt @@ -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() 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() val castMap = remember { mainViewModel.tvCast } diff --git a/app/src/main/java/com/owenlejeune/tvtime/ui/viewmodel/MainViewModel.kt b/app/src/main/java/com/owenlejeune/tvtime/ui/viewmodel/MainViewModel.kt index cf9b96e..2b42043 100644 --- a/app/src/main/java/com/owenlejeune/tvtime/ui/viewmodel/MainViewModel.kt +++ b/app/src/main/java/com/owenlejeune/tvtime/ui/viewmodel/MainViewModel.kt @@ -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) { + 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 + } + } \ No newline at end of file