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, INACTIVE,
LOADING, LOADING,
REFRESHING,
COMPLETE, COMPLETE,
ERROR ERROR

View File

@@ -1,5 +1,6 @@
package com.owenlejeune.tvtime.api package com.owenlejeune.tvtime.api
import androidx.compose.runtime.MutableState
import retrofit2.Response import retrofit2.Response
infix fun <T> Response<T>.storedIn(body: (T) -> Unit) { infix fun <T> Response<T>.storedIn(body: (T) -> Unit) {
@@ -9,3 +10,23 @@ infix fun <T> Response<T>.storedIn(body: (T) -> Unit) {
} }
} }
} }
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 { 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 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 postRating(id: Int, ratingBody: RatingBody)
suspend fun deleteRating(id: Int) 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) suspend fun getAccountStates(id: Int)

View File

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

View File

@@ -1,7 +1,9 @@
package com.owenlejeune.tvtime.api.tmdb.api.v3 package com.owenlejeune.tvtime.api.tmdb.api.v3
import android.util.Log
import androidx.compose.runtime.mutableStateMapOf 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.TmdbClient
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.DetailCast import com.owenlejeune.tvtime.api.tmdb.api.v3.model.DetailCast
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.DetailCrew 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 imagesMap = Collections.synchronizedMap(mutableStateMapOf<Int, List<PersonImage>>())
val externalIdsMap = Collections.synchronizedMap(mutableStateMapOf<Int, ExternalIds>()) val externalIdsMap = Collections.synchronizedMap(mutableStateMapOf<Int, ExternalIds>())
suspend fun getPerson(id: Int) { val detailsLoadingState = mutableStateOf(LoadingState.INACTIVE)
val response = service.getPerson(id) val castCrewLoadingState = mutableStateOf(LoadingState.INACTIVE)
if (response.isSuccessful) { val imagesLoadingState = mutableStateOf(LoadingState.INACTIVE)
response.body()?.let { val externalIdsLoadingState = mutableStateOf(LoadingState.INACTIVE)
Log.d(TAG, "Successfully got person $id")
peopleMap[id] = it suspend fun getPerson(id: Int, refreshing: Boolean) {
} ?: run { loadRemoteData(
Log.w(TAG, "Problem getting person $id") { service.getPerson(id) },
} { peopleMap[id] = it },
} else { detailsLoadingState,
Log.e(TAG, "Issue getting person $id") refreshing
} )
} }
suspend fun getCredits(id: Int) { suspend fun getCredits(id: Int, refreshing: Boolean) {
val response = service.getCredits(id) loadRemoteData(
if (response.isSuccessful) { { service.getCredits(id) },
response.body()?.let { {
Log.d(TAG, "Successfully got credits $id")
castMap[id] = it.cast castMap[id] = it.cast
crewMap[id] = it.crew crewMap[id] = it.crew
} ?: run { },
Log.w(TAG, "Problem getting credits $id") castCrewLoadingState,
} refreshing
} else { )
Log.e(TAG, "Issue getting credits $id")
}
} }
suspend fun getImages(id: Int) { suspend fun getImages(id: Int, refreshing: Boolean) {
val response = service.getImages(id) loadRemoteData(
if (response.isSuccessful) { { service.getImages(id) },
response.body()?.let { { imagesMap[id] = it.images },
Log.d(TAG, "Successfully got images $id") imagesLoadingState,
imagesMap[id] = it.images refreshing
} ?: run { )
Log.w(TAG, "Problem getting images $id")
}
} else {
Log.e(TAG, "Issues getting images $id")
}
} }
suspend fun getExternalIds(id: Int) { suspend fun getExternalIds(id: Int, refreshing: Boolean) {
val response = service.getExternalIds(id) loadRemoteData(
if (response.isSuccessful) { { service.getExternalIds(id) },
response.body()?.let { { externalIdsMap[id] = it },
Log.d(TAG, "Successfully got external ids $id") externalIdsLoadingState,
externalIdsMap[id] = it refreshing
} ?: run { )
Log.w(TAG, "Problem getting external ids $id")
}
} else {
Log.e(TAG, "Issue getting external ids $id")
}
} }
suspend fun getPopular(page: Int): Response<HomePagePeopleResponse> { 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 android.util.Log
import androidx.compose.runtime.mutableStateMapOf import androidx.compose.runtime.mutableStateMapOf
import androidx.compose.runtime.mutableStateOf
import androidx.paging.PagingData import androidx.paging.PagingData
import androidx.paging.PagingSource import androidx.paging.PagingSource
import androidx.paging.PagingState 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.storedIn
import com.owenlejeune.tvtime.api.tmdb.TmdbClient import com.owenlejeune.tvtime.api.tmdb.TmdbClient
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.AccountStates 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>> val seasons: MutableMap<Int, out Set<Season>>
get() = _seasons get() = _seasons
override suspend fun getById(id: Int) { val detailsLoadingState = mutableStateOf(LoadingState.INACTIVE)
service.getTvShowById(id) storedIn { detailTv[id] = it } 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) { override suspend fun getImages(id: Int, refreshing: Boolean) {
service.getTvImages(id) storedIn { images[id] = it } loadRemoteData(
{ service.getTvImages(id) },
{ images[id] = it },
imagesLoadingState,
refreshing
)
} }
override suspend fun getCastAndCrew(id: Int) { override suspend fun getCastAndCrew(id: Int, refreshing: Boolean) {
service.getCastAndCrew(id) storedIn { loadRemoteData(
{ service.getCastAndCrew(id) },
{
cast[id] = it.cast cast[id] = it.cast
crew[id] = it.crew crew[id] = it.crew
} },
castCrewLoadingState,
refreshing
)
} }
suspend fun getContentRatings(id: Int) { suspend fun getContentRatings(id: Int, refreshing: Boolean) {
service.getContentRatings(id) storedIn { contentRatings[id] = it.results } loadRemoteData(
{ service.getContentRatings(id) },
{ contentRatings[id] = it.results },
contentRatingsLoadingState,
refreshing
)
} }
override suspend fun getVideos(id: Int) { override suspend fun getVideos(id: Int, refreshing: Boolean) {
service.getVideos(id) storedIn { videos[id] = it.results } loadRemoteData(
{ service.getVideos(id) },
{ videos[id] = it.results },
videosLoadingState,
refreshing
)
} }
override suspend fun getReviews(id: Int) { override suspend fun getReviews(id: Int, refreshing: Boolean) {
service.getReviews(id) storedIn { reviews[id] = it.results } loadRemoteData(
{ service.getReviews(id) },
{ reviews[id] = it.results },
reviewsLoadingState,
refreshing
)
} }
override suspend fun getKeywords(id: Int) { override suspend fun getKeywords(id: Int, refreshing: Boolean) {
service.getKeywords(id) storedIn { keywords[id] = it.keywords ?: emptyList() } loadRemoteData(
{ service.getKeywords(id) },
{ keywords[id] = it.keywords ?: emptyList() },
keywordsLoadingState,
refreshing
)
} }
override suspend fun getWatchProviders(id: Int) { override suspend fun getWatchProviders(id: Int, refreshing: Boolean) {
service.getWatchProviders(id) storedIn { loadRemoteData(
{ service.getWatchProviders(id) },
{
it.results[Locale.getDefault().country]?.let { wp -> it.results[Locale.getDefault().country]?.let { wp ->
watchProviders[id] = wp watchProviders[id] = wp
} }
} },
watchProvidersLoadingState,
refreshing
)
} }
override suspend fun getAccountStates(id: Int) { 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) { suspend fun getSeason(seriesId: Int, seasonId: Int, refreshing: Boolean) {
service.getSeason(seriesId, seasonId) storedIn { loadRemoteData(
{ service.getSeason(seriesId, seasonId) },
{
_seasons[seriesId]?.add(it) ?: run { _seasons[seriesId]?.add(it) ?: run {
_seasons[seriesId] = mutableSetOf(it) _seasons[seriesId] = mutableSetOf(it)
} }
} },
seasonsLoadingState,
refreshing
)
} }
override suspend fun getExternalIds(id: Int) { override suspend fun getExternalIds(id: Int, refreshing: Boolean) {
service.getExternalIds(id) storedIn { externalIds[id] = it } loadRemoteData(
{ service.getExternalIds(id) },
{ externalIds[id] = it },
externalIdsLoadingState,
refreshing
)
} }
override suspend fun postRating(id: Int, ratingBody: RatingBody) { override suspend fun postRating(id: Int, ratingBody: RatingBody) {

View File

@@ -7,3 +7,5 @@ import kotlinx.coroutines.launch
fun Any.coroutineTask(runnable: suspend () -> Unit) { fun Any.coroutineTask(runnable: suspend () -> Unit) {
CoroutineScope(Dispatchers.IO).launch { runnable() } 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.layout.onGloballyPositioned
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
@@ -252,11 +253,13 @@ private fun ExternalIdLogo(
context.startActivity(intent) context.startActivity(intent)
}, },
modifier = Modifier.size(28.dp) modifier = Modifier.size(28.dp)
// modifier = Modifier.size(40.dp)
) { ) {
Icon( Icon(
painter = logoPainter, painter = logoPainter,
contentDescription = null, 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.content.Intent
import android.net.Uri import android.net.Uri
import android.widget.Toast import android.widget.Toast
import androidx.compose.animation.* import androidx.compose.animation.Crossfade
import androidx.compose.foundation.* import androidx.compose.foundation.background
import androidx.compose.foundation.layout.* 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.lazy.LazyRow
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape 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.Icons
import androidx.compose.material.icons.filled.ArrowBack import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material.icons.filled.Movie import androidx.compose.material.icons.filled.Movie
import androidx.compose.material.icons.filled.Send import androidx.compose.material.icons.filled.Send
import androidx.compose.material3.* import androidx.compose.material.pullrefresh.PullRefreshIndicator
import androidx.compose.runtime.* 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.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip 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.pager.rememberPagerState
import com.google.accompanist.systemuicontroller.rememberSystemUiController import com.google.accompanist.systemuicontroller.rememberSystemUiController
import com.owenlejeune.tvtime.R 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.DateFormat
import com.owenlejeune.tvtime.extensions.WindowSizeClass import com.owenlejeune.tvtime.extensions.WindowSizeClass
import com.owenlejeune.tvtime.extensions.format import com.owenlejeune.tvtime.extensions.format
@@ -47,17 +97,63 @@ import com.owenlejeune.tvtime.extensions.getCalendarYear
import com.owenlejeune.tvtime.extensions.lazyPagingItems import com.owenlejeune.tvtime.extensions.lazyPagingItems
import com.owenlejeune.tvtime.extensions.listItems import com.owenlejeune.tvtime.extensions.listItems
import com.owenlejeune.tvtime.preferences.AppPreferences 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.navigation.AppNavItem
import com.owenlejeune.tvtime.ui.viewmodel.MainViewModel import com.owenlejeune.tvtime.ui.viewmodel.MainViewModel
import com.owenlejeune.tvtime.ui.viewmodel.SpecialFeaturesViewModel import com.owenlejeune.tvtime.ui.viewmodel.SpecialFeaturesViewModel
import com.owenlejeune.tvtime.utils.SessionManager import com.owenlejeune.tvtime.utils.SessionManager
import com.owenlejeune.tvtime.utils.TmdbUtils import com.owenlejeune.tvtime.utils.TmdbUtils
import com.owenlejeune.tvtime.utils.types.MediaViewType import com.owenlejeune.tvtime.utils.types.MediaViewType
import kotlinx.coroutines.* import kotlinx.coroutines.launch
import org.koin.java.KoinJavaComponent.get 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 @Composable
fun MediaDetailScreen( fun MediaDetailScreen(
appNavController: NavController, appNavController: NavController,
@@ -65,17 +161,14 @@ fun MediaDetailScreen(
type: MediaViewType, type: MediaViewType,
windowSize: WindowSizeClass windowSize: WindowSizeClass
) { ) {
val scope = rememberCoroutineScope()
val mainViewModel = viewModel<MainViewModel>() val mainViewModel = viewModel<MainViewModel>()
val systemUiController = rememberSystemUiController() val systemUiController = rememberSystemUiController()
systemUiController.setStatusBarColor(color = MaterialTheme.colorScheme.background) systemUiController.setStatusBarColor(color = MaterialTheme.colorScheme.background)
systemUiController.setNavigationBarColor(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 mediaItems: Map<Int, DetailedItem> = remember { mainViewModel.produceDetailsFor(type) }
val mediaItem = mediaItems[itemId] val mediaItem = mediaItems[itemId]
@@ -87,6 +180,30 @@ fun MediaDetailScreen(
val pagerState = rememberPagerState(initialPage = 0) 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( Box(
modifier = Modifier.fillMaxSize() modifier = Modifier.fillMaxSize()
) { ) {
@@ -116,7 +233,10 @@ fun MediaDetailScreen(
) )
} }
) { innerPadding -> ) { innerPadding ->
Box(modifier = Modifier.padding(innerPadding)) { Box(modifier = Modifier
.padding(innerPadding)
.pullRefresh(state = pullRefreshState)
) {
MediaViewContent( MediaViewContent(
appNavController = appNavController, appNavController = appNavController,
itemId = itemId, itemId = itemId,
@@ -128,6 +248,11 @@ fun MediaDetailScreen(
pagerState = pagerState, pagerState = pagerState,
mainViewModel = mainViewModel 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 @Composable
private fun MediaViewContent( private fun MediaViewContent(
appNavController: NavController, appNavController: NavController,
@@ -157,10 +282,6 @@ private fun MediaViewContent(
pagerState: PagerState, pagerState: PagerState,
preferences: AppPreferences = get(AppPreferences::class.java) preferences: AppPreferences = get(AppPreferences::class.java)
) { ) {
LaunchedEffect(Unit) {
mainViewModel.getExternalIds(itemId, type)
}
Row( Row(
modifier = Modifier modifier = Modifier
.background(color = MaterialTheme.colorScheme.background), .background(color = MaterialTheme.colorScheme.background),
@@ -290,9 +411,6 @@ private fun MiscTvDetails(
mainViewModel: MainViewModel mainViewModel: MainViewModel
) { ) {
mediaItem?.let { tv -> mediaItem?.let { tv ->
LaunchedEffect(Unit) {
mainViewModel.getContentRatings(itemId)
}
val series = tv as DetailedTv val series = tv as DetailedTv
val contentRatingsMap = remember { mainViewModel.tvContentRatings } val contentRatingsMap = remember { mainViewModel.tvContentRatings }
@@ -318,10 +436,6 @@ private fun MiscMovieDetails(
mainViewModel: MainViewModel mainViewModel: MainViewModel
) { ) {
mediaItem?.let { mi -> mediaItem?.let { mi ->
LaunchedEffect(Unit) {
mainViewModel.getReleaseDates(itemId)
}
val movie = mi as DetailedMovie val movie = mi as DetailedMovie
val contentRatingsMap = remember { mainViewModel.movieReleaseDates } val contentRatingsMap = remember { mainViewModel.movieReleaseDates }
@@ -388,10 +502,6 @@ private fun OverviewCard(
appNavController: NavController, appNavController: NavController,
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
LaunchedEffect(Unit) {
mainViewModel.getKeywords(itemId, type)
}
val keywordsMap = remember { mainViewModel.produceKeywordsFor(type) } val keywordsMap = remember { mainViewModel.produceKeywordsFor(type) }
val keywords = keywordsMap[itemId] val keywords = keywordsMap[itemId]
@@ -602,10 +712,6 @@ private fun CastCard(
appNavController: NavController, appNavController: NavController,
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
LaunchedEffect(Unit) {
mainViewModel.getCastAndCrew(itemId, type)
}
val castMap = remember { mainViewModel.produceCastFor(type) } val castMap = remember { mainViewModel.produceCastFor(type) }
val cast = castMap[itemId] val cast = castMap[itemId]
@@ -694,13 +800,6 @@ private fun SeasonCard(
mainViewModel: MainViewModel, mainViewModel: MainViewModel,
appNavController: NavController appNavController: NavController
) { ) {
LaunchedEffect(mediaItem) {
val lastSeason = (mediaItem as DetailedTv?)?.numberOfSeasons ?: 0
if (lastSeason > 0) {
mainViewModel.getSeason(itemId, lastSeason)
}
}
val seasonsMap = remember { mainViewModel.tvSeasons } val seasonsMap = remember { mainViewModel.tvSeasons }
val lastSeason = seasonsMap[itemId]?.lastOrNull() val lastSeason = seasonsMap[itemId]?.lastOrNull()
@@ -766,10 +865,6 @@ fun SimilarContentCard(
appNavController: NavController, appNavController: NavController,
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
LaunchedEffect(Unit) {
mainViewModel.getSimilar(itemId, mediaType)
}
val similarContentMap = remember { mainViewModel.produceSimilarContentFor(mediaType) } val similarContentMap = remember { mainViewModel.produceSimilarContentFor(mediaType) }
val similarContent = similarContentMap[itemId] val similarContent = similarContentMap[itemId]
val pagingItems = similarContent?.collectAsLazyPagingItems() val pagingItems = similarContent?.collectAsLazyPagingItems()
@@ -822,10 +917,6 @@ fun VideosCard(
mainViewModel: MainViewModel, mainViewModel: MainViewModel,
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
LaunchedEffect(Unit) {
mainViewModel.getVideos(itemId, type)
}
val videosMap = remember { mainViewModel.produceVideosFor(type) } val videosMap = remember { mainViewModel.produceVideosFor(type) }
val videos = videosMap[itemId] val videos = videosMap[itemId]
@@ -902,10 +993,6 @@ private fun WatchProvidersCard(
mainViewModel: MainViewModel, mainViewModel: MainViewModel,
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
LaunchedEffect(Unit) {
mainViewModel.getWatchProviders(itemId, type)
}
val watchProvidersMap = remember { mainViewModel.produceWatchProvidersFor(type) } val watchProvidersMap = remember { mainViewModel.produceWatchProvidersFor(type) }
val watchProviders = watchProvidersMap[itemId] val watchProviders = watchProvidersMap[itemId]
watchProviders?.let { providers -> watchProviders?.let { providers ->
@@ -1116,10 +1203,6 @@ private fun ReviewsCard(
mainViewModel: MainViewModel, mainViewModel: MainViewModel,
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
LaunchedEffect(Unit) {
mainViewModel.getReviews(itemId, type)
}
val reviewsMap = remember { mainViewModel.produceReviewsFor(type) } val reviewsMap = remember { mainViewModel.produceReviewsFor(type) }
val reviews = reviewsMap[itemId] val reviews = reviewsMap[itemId]

View File

@@ -1,5 +1,6 @@
package com.owenlejeune.tvtime.ui.screens package com.owenlejeune.tvtime.ui.screens
import android.graphics.Paint.Align
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement 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.lazy.items
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material.icons.filled.Person 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.ExperimentalMaterial3Api
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
@@ -28,7 +33,10 @@ import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.rememberTopAppBarState import androidx.compose.material3.rememberTopAppBarState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.res.stringResource 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.ui.viewmodel.MainViewModel
import com.owenlejeune.tvtime.utils.TmdbUtils import com.owenlejeune.tvtime.utils.TmdbUtils
import com.owenlejeune.tvtime.utils.types.MediaViewType import com.owenlejeune.tvtime.utils.types.MediaViewType
import kotlinx.coroutines.launch
import java.lang.Integer.min import java.lang.Integer.min
private const val TAG = "PeopleDetailScreen" 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 @Composable
fun PersonDetailScreen( fun PersonDetailScreen(
appNavController: NavController, appNavController: NavController,
personId: Int personId: Int
) { ) {
val scope = rememberCoroutineScope()
val mainViewModel = viewModel<MainViewModel>() val mainViewModel = viewModel<MainViewModel>()
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
mainViewModel.getById(personId, MediaViewType.PERSON) fetchData(mainViewModel, personId)
mainViewModel.getExternalIds(personId, MediaViewType.PERSON)
mainViewModel.getCastAndCrew(personId, MediaViewType.PERSON)
mainViewModel.getImages(personId, MediaViewType.PERSON)
} }
val systemUiController = rememberSystemUiController() val systemUiController = rememberSystemUiController()
@@ -79,6 +98,17 @@ fun PersonDetailScreen(
val topAppBarScrollState = rememberTopAppBarState() val topAppBarScrollState = rememberTopAppBarState()
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(topAppBarScrollState) 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( Scaffold(
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
topBar = { topBar = {
@@ -102,7 +132,10 @@ fun PersonDetailScreen(
) )
} }
) { innerPadding -> ) { innerPadding ->
Box(modifier = Modifier.padding(innerPadding)) { Box(modifier = Modifier
.padding(innerPadding)
.pullRefresh(state = pullRefreshState)
) {
Column( Column(
modifier = Modifier modifier = Modifier
.background(color = MaterialTheme.colorScheme.background) .background(color = MaterialTheme.colorScheme.background)
@@ -131,6 +164,12 @@ fun PersonDetailScreen(
ImagesCard(id = personId, appNavController = appNavController) 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 modifier = Modifier
.padding(start = 16.dp, bottom = 16.dp) .padding(start = 16.dp, bottom = 16.dp)
.clickable { .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) service: MoviesService = get(MoviesService::class.java)
) { ) {
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
service.getCastAndCrew(result.id) service.getCastAndCrew(result.id, false)
} }
val mainViewModel = viewModel<MainViewModel>() val mainViewModel = viewModel<MainViewModel>()
val castMap = remember { mainViewModel.movieCast } val castMap = remember { mainViewModel.movieCast }
@@ -428,7 +428,7 @@ private fun TvSearchResultView(
) { ) {
val context = LocalContext.current val context = LocalContext.current
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
service.getCastAndCrew(result.id) service.getCastAndCrew(result.id, false)
} }
val mainViewModel = viewModel<MainViewModel>() val mainViewModel = viewModel<MainViewModel>()
val castMap = remember { mainViewModel.tvCast } val castMap = remember { mainViewModel.tvCast }

View File

@@ -1,7 +1,12 @@
package com.owenlejeune.tvtime.ui.viewmodel 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.lifecycle.ViewModel
import androidx.paging.PagingData 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.createPagingFlow
import com.owenlejeune.tvtime.api.tmdb.api.v3.MoviesService import com.owenlejeune.tvtime.api.tmdb.api.v3.MoviesService
import com.owenlejeune.tvtime.api.tmdb.api.v3.PeopleService 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.TmdbItem
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.Video import com.owenlejeune.tvtime.api.tmdb.api.v3.model.Video
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.WatchProviders 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.ui.screens.tabs.MediaTabNavItem
import com.owenlejeune.tvtime.utils.types.MediaViewType import com.owenlejeune.tvtime.utils.types.MediaViewType
import com.owenlejeune.tvtime.utils.types.TimeWindow import com.owenlejeune.tvtime.utils.types.TimeWindow
@@ -47,6 +53,17 @@ class MainViewModel: ViewModel(), KoinComponent {
val movieAccountStates = movieService.accountStates val movieAccountStates = movieService.accountStates
val movieKeywordResults = movieService.keywordResults 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 { val popularMovies by lazy {
createPagingFlow( createPagingFlow(
fetcher = { p -> movieService.getPopular(p) }, fetcher = { p -> movieService.getPopular(p) },
@@ -87,6 +104,18 @@ class MainViewModel: ViewModel(), KoinComponent {
val tvAccountStates = tvService.accountStates val tvAccountStates = tvService.accountStates
val tvKeywordResults = tvService.keywordResults 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 { val popularTv by lazy {
createPagingFlow( createPagingFlow(
fetcher = { p -> tvService.getPopular(p) }, fetcher = { p -> tvService.getPopular(p) },
@@ -118,6 +147,11 @@ class MainViewModel: ViewModel(), KoinComponent {
val peopleImagesMap = peopleService.imagesMap val peopleImagesMap = peopleService.imagesMap
val peopleExternalIdsMap = peopleService.externalIdsMap val peopleExternalIdsMap = peopleService.externalIdsMap
val peopleDetailsLoadingState = peopleService.detailsLoadingState
val peopleCastCrewLoadingState = peopleService.castCrewLoadingState
val peopleImagesLoadingState = peopleService.imagesLoadingState
val peopleExternalIdsLoadingState = peopleService.externalIdsLoadingState
val popularPeople by lazy { val popularPeople by lazy {
createPagingFlow( createPagingFlow(
fetcher = { p -> peopleService.getPopular(p) }, fetcher = { p -> peopleService.getPopular(p) },
@@ -231,68 +265,68 @@ class MainViewModel: ViewModel(), KoinComponent {
suspend fun getById(id: Int, type: MediaViewType, force: Boolean = false) { suspend fun getById(id: Int, type: MediaViewType, force: Boolean = false) {
when (type) { when (type) {
MediaViewType.MOVIE -> if (detailMovies[id] == null || force) movieService.getById(id) MediaViewType.MOVIE -> if (detailMovies[id] == null || force) movieService.getById(id, force)
MediaViewType.TV -> if (detailedTv[id] == null || force) tvService.getById(id) MediaViewType.TV -> if (detailedTv[id] == null || force) tvService.getById(id, force)
MediaViewType.PERSON -> if (peopleMap[id] == null || force) peopleService.getPerson(id) MediaViewType.PERSON -> if (peopleMap[id] == null || force) peopleService.getPerson(id, force)
else -> {} else -> {}
} }
} }
suspend fun getImages(id: Int, type: MediaViewType, force: Boolean = false) { suspend fun getImages(id: Int, type: MediaViewType, force: Boolean = false) {
when (type) { when (type) {
MediaViewType.MOVIE -> if (movieImages[id] == null || force) movieService.getImages(id) MediaViewType.MOVIE -> if (movieImages[id] == null || force) movieService.getImages(id, force)
MediaViewType.TV -> if (tvImages[id] == null || force) tvService.getImages(id) MediaViewType.TV -> if (tvImages[id] == null || force) tvService.getImages(id, force)
MediaViewType.PERSON -> if (peopleImagesMap[id] == null || force) peopleService.getImages(id) MediaViewType.PERSON -> if (peopleImagesMap[id] == null || force) peopleService.getImages(id, force)
else -> {} else -> {}
} }
} }
suspend fun getCastAndCrew(id: Int, type: MediaViewType, force: Boolean = false) { suspend fun getCastAndCrew(id: Int, type: MediaViewType, force: Boolean = false) {
when (type) { when (type) {
MediaViewType.MOVIE -> if (movieCast[id] == null || movieCrew[id] == null || force) movieService.getCastAndCrew(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) 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) MediaViewType.PERSON -> if (peopleCastMap[id] == null || peopleCrewMap[id] == null || force) peopleService.getCredits(id, force)
else -> {} else -> {}
} }
} }
suspend fun getVideos(id: Int, type: MediaViewType, force: Boolean = false) { suspend fun getVideos(id: Int, type: MediaViewType, force: Boolean = false) {
when (type) { when (type) {
MediaViewType.MOVIE -> if (movieVideos[id] == null || force) movieService.getVideos(id) MediaViewType.MOVIE -> if (movieVideos[id] == null || force) movieService.getVideos(id, force)
MediaViewType.TV -> if (tvVideos[id] == null || force) tvService.getVideos(id) MediaViewType.TV -> if (tvVideos[id] == null || force) tvService.getVideos(id, force)
else -> {} else -> {}
} }
} }
suspend fun getReviews(id: Int, type: MediaViewType, force: Boolean = false) { suspend fun getReviews(id: Int, type: MediaViewType, force: Boolean = false) {
when (type) { when (type) {
MediaViewType.MOVIE -> if (movieReviews[id] == null || force) movieService.getReviews(id) MediaViewType.MOVIE -> if (movieReviews[id] == null || force) movieService.getReviews(id, force)
MediaViewType.TV -> if (tvReviews[id] == null || force) tvService.getReviews(id) MediaViewType.TV -> if (tvReviews[id] == null || force) tvService.getReviews(id, force)
else -> {} else -> {}
} }
} }
suspend fun getKeywords(id: Int, type: MediaViewType, force: Boolean = false) { suspend fun getKeywords(id: Int, type: MediaViewType, force: Boolean = false) {
when (type) { when (type) {
MediaViewType.MOVIE -> if (movieKeywords[id] == null || force) movieService.getKeywords(id) MediaViewType.MOVIE -> if (movieKeywords[id] == null || force) movieService.getKeywords(id, force)
MediaViewType.TV -> if (tvKeywords[id] == null || force) tvService.getKeywords(id) MediaViewType.TV -> if (tvKeywords[id] == null || force) tvService.getKeywords(id, force)
else -> {} else -> {}
} }
} }
suspend fun getWatchProviders(id: Int, type: MediaViewType, force: Boolean = false) { suspend fun getWatchProviders(id: Int, type: MediaViewType, force: Boolean = false) {
when (type) { when (type) {
MediaViewType.MOVIE -> if (movieWatchProviders[id] == null || force) movieService.getWatchProviders(id) MediaViewType.MOVIE -> if (movieWatchProviders[id] == null || force) movieService.getWatchProviders(id, force)
MediaViewType.TV -> if (tvWatchProviders[id] == null || force) tvService.getWatchProviders(id) MediaViewType.TV -> if (tvWatchProviders[id] == null || force) tvService.getWatchProviders(id, force)
else -> {} else -> {}
} }
} }
suspend fun getExternalIds(id: Int, type: MediaViewType, force: Boolean = false) { suspend fun getExternalIds(id: Int, type: MediaViewType, force: Boolean = false) {
when (type) { when (type) {
MediaViewType.MOVIE -> if (movieExternalIds[id] == null || force) movieService.getExternalIds(id) MediaViewType.MOVIE -> if (movieExternalIds[id] == null || force) movieService.getExternalIds(id, force)
MediaViewType.TV -> if (tvExternalIds[id] == null || force) tvService.getExternalIds(id) MediaViewType.TV -> if (tvExternalIds[id] == null || force) tvService.getExternalIds(id, force)
MediaViewType.PERSON -> if (peopleExternalIdsMap[id] == null || force) peopleService.getExternalIds(id) MediaViewType.PERSON -> if (peopleExternalIdsMap[id] == null || force) peopleService.getExternalIds(id, force)
else -> {} else -> {}
} }
} }
@@ -359,20 +393,64 @@ class MainViewModel: ViewModel(), KoinComponent {
suspend fun getReleaseDates(id: Int, force: Boolean = false) { suspend fun getReleaseDates(id: Int, force: Boolean = false) {
if (movieReleaseDates[id] == null || force) { if (movieReleaseDates[id] == null || force) {
movieService.getReleaseDates(id) movieService.getReleaseDates(id, force)
} }
} }
suspend fun getContentRatings(id: Int, force: Boolean = false) { suspend fun getContentRatings(id: Int, force: Boolean = false) {
if (tvContentRatings[id] == null || force) { if (tvContentRatings[id] == null || force) {
tvService.getContentRatings(id) tvService.getContentRatings(id, force)
} }
} }
suspend fun getSeason(seriesId: Int, seasonId: Int, force: Boolean = false) { suspend fun getSeason(seriesId: Int, seasonId: Int, force: Boolean = false) {
if (tvSeasons[seriesId] == null || force) { 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
}
} }