From ec12621743888d7a2af6954f4cc53d61321ab4bb Mon Sep 17 00:00:00 2001 From: Owen LeJeune Date: Wed, 31 Aug 2022 23:25:53 -0400 Subject: [PATCH] refactor support for paging backdrop images --- .../owenlejeune/tvtime/api/tmdb/TmdbClient.kt | 25 ++- .../extensions/InterceptorExtensions.kt | 19 --- .../tvtime/preferences/AppPreferences.kt | 4 + .../tvtime/ui/components/Posters.kt | 59 ------- .../ui/screens/main/DetailViewCommon.kt | 160 +++++++++++++----- .../tvtime/ui/screens/main/MediaDetailView.kt | 17 +- 6 files changed, 160 insertions(+), 124 deletions(-) diff --git a/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/TmdbClient.kt b/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/TmdbClient.kt index f1595ac..53db95d 100644 --- a/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/TmdbClient.kt +++ b/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/TmdbClient.kt @@ -77,15 +77,20 @@ class TmdbClient: KoinComponent { override fun intercept(chain: Interceptor.Chain): Response { val apiParam = QueryParam("api_key", BuildConfig.TMDB_ApiKey) - val locale = Locale.current - val languageCode = "${locale.language}-${locale.region}" - val languageParam = QueryParam("language", languageCode) - val segments = chain.request().url.encodedPathSegments val sessionIdParam: QueryParam? = sessionIdParam(segments) val builder = chain.request().url.newBuilder() - builder.addQueryParams(apiParam, languageParam, sessionIdParam) + builder.addQueryParams(apiParam, sessionIdParam) + + if (shouldIncludeLanguageParam(segments)) { + val locale = Locale.current + val languageCode = "${locale.language}-${locale.region}" + val languageParam = QueryParam("language", languageCode) + + builder.addQueryParams(languageParam) + } + val requestBuilder = chain.request().newBuilder().url(builder.build()) val request = requestBuilder.build() @@ -106,6 +111,16 @@ class TmdbClient: KoinComponent { } return sessionIdParam } + + private fun shouldIncludeLanguageParam(urlSegments: List): Boolean { + val ignoredRoutes = listOf("images") + for (route in ignoredRoutes) { + if (urlSegments.contains(route)) { + return false + } + } + return true + } } private inner class V4Interceptor: Interceptor { diff --git a/app/src/main/java/com/owenlejeune/tvtime/extensions/InterceptorExtensions.kt b/app/src/main/java/com/owenlejeune/tvtime/extensions/InterceptorExtensions.kt index 1a683d6..736b5fb 100644 --- a/app/src/main/java/com/owenlejeune/tvtime/extensions/InterceptorExtensions.kt +++ b/app/src/main/java/com/owenlejeune/tvtime/extensions/InterceptorExtensions.kt @@ -4,25 +4,6 @@ import com.owenlejeune.tvtime.api.QueryParam import okhttp3.HttpUrl import okhttp3.Request -// -//fun Interceptor.Chain.addQueryParams(vararg queryParams: QueryParam?): Request { -// val original = request() -// val originalHttpUrl = original.url -// -// val urlBuilder = originalHttpUrl.newBuilder() -// queryParams.forEach { param -> -// if (param != null) { -// urlBuilder.addQueryParameter(param.key, param.param) -// } -// } -// val url = urlBuilder.build() -// -// val requestBuilder = original.newBuilder() -// .url(url) -// -// return requestBuilder.build() -//} - fun HttpUrl.Builder.addQueryParams(vararg queryParams: QueryParam?): HttpUrl.Builder { return apply { queryParams.forEach { param -> diff --git a/app/src/main/java/com/owenlejeune/tvtime/preferences/AppPreferences.kt b/app/src/main/java/com/owenlejeune/tvtime/preferences/AppPreferences.kt index fd5027f..81f6395 100644 --- a/app/src/main/java/com/owenlejeune/tvtime/preferences/AppPreferences.kt +++ b/app/src/main/java/com/owenlejeune/tvtime/preferences/AppPreferences.kt @@ -6,6 +6,7 @@ import com.google.gson.Gson import com.kieronquinn.monetcompat.core.MonetCompat import com.owenlejeune.tvtime.BuildConfig import com.owenlejeune.tvtime.utils.SessionManager +import java.util.function.BiPredicate class AppPreferences(context: Context) { @@ -24,6 +25,7 @@ class AppPreferences(context: Context) { private val USE_SYSTEM_COLORS = "use_system_colors" private val SELECTED_COLOR = "selected_color" private val USE_V4_API = "use_v4_api" + private val SHOW_BACKDROP_GALLERY = "show_backdrop_gallery" } private val preferences: SharedPreferences = context.getSharedPreferences(PREF_FILE, Context.MODE_PRIVATE) @@ -78,6 +80,8 @@ class AppPreferences(context: Context) { get() = preferences.getBoolean(USE_V4_API, true) set(value) { preferences.put(USE_V4_API, value) } + var showBackdropGallery: Boolean = true + private fun SharedPreferences.put(key: String, value: Any?) { edit().apply { when (value) { diff --git a/app/src/main/java/com/owenlejeune/tvtime/ui/components/Posters.kt b/app/src/main/java/com/owenlejeune/tvtime/ui/components/Posters.kt index 5be71e9..d3dba58 100644 --- a/app/src/main/java/com/owenlejeune/tvtime/ui/components/Posters.kt +++ b/app/src/main/java/com/owenlejeune/tvtime/ui/components/Posters.kt @@ -183,63 +183,4 @@ fun PosterItem( ) } } -} - -@SuppressLint("CoroutineCreationDuringComposition") -@OptIn(ExperimentalPagerApi::class) -@Composable -fun BackdropImage( - modifier: Modifier = Modifier, - imageUrl: String? = null, - collection: ImageCollection? = null, - contentDescription: String? = null -) { - val context = LocalContext.current - - var sizeImage by remember { mutableStateOf(IntSize.Zero) } - - val gradient = Brush.verticalGradient( - colors = listOf(Color.Transparent, MaterialTheme.colorScheme.background), - startY = sizeImage.height.toFloat() / 3, - endY = sizeImage.height.toFloat() - ) - - Box( - modifier = modifier - ) { - if (collection != null) { - val pagerState = rememberPagerState() - HorizontalPager(count = collection.backdrops.size, state = pagerState) { page -> - val backdrop = collection.backdrops[page] - AsyncImage( - model = TmdbUtils.getFullBackdropPath(backdrop), - placeholder = rememberAsyncImagePainter(model = R.drawable.placeholder), - contentDescription = "", - modifier = Modifier.onGloballyPositioned { sizeImage = it.size } - ) - } - } else { - if (imageUrl != null) { - AsyncImage( - model = imageUrl, - placeholder = rememberAsyncImagePainter(model = R.drawable.placeholder), - contentDescription = contentDescription, - modifier = Modifier.onGloballyPositioned { sizeImage = it.size }, - contentScale = ContentScale.FillWidth - ) - } else { - Image( - painter = rememberAsyncImagePainter(model = R.drawable.placeholder), - contentDescription = contentDescription, - modifier = Modifier.onGloballyPositioned { sizeImage = it.size }, - contentScale = ContentScale.FillWidth - ) - } - } - Box( - modifier = Modifier - .matchParentSize() - .background(gradient) - ) - } } \ No newline at end of file diff --git a/app/src/main/java/com/owenlejeune/tvtime/ui/screens/main/DetailViewCommon.kt b/app/src/main/java/com/owenlejeune/tvtime/ui/screens/main/DetailViewCommon.kt index 7dc0121..b5198b8 100644 --- a/app/src/main/java/com/owenlejeune/tvtime/ui/screens/main/DetailViewCommon.kt +++ b/app/src/main/java/com/owenlejeune/tvtime/ui/screens/main/DetailViewCommon.kt @@ -1,38 +1,42 @@ package com.owenlejeune.tvtime.ui.screens.main +import androidx.compose.animation.core.FastOutSlowInEasing +import androidx.compose.animation.core.tween +import androidx.compose.foundation.Image import androidx.compose.foundation.background -import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.* -import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.CircleShape -import androidx.compose.foundation.verticalScroll -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.ArrowBack -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable +import androidx.compose.runtime.* import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.layout.onGloballyPositioned +import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.constraintlayout.compose.ConstraintLayout -import androidx.navigation.NavController +import coil.compose.AsyncImage +import coil.compose.rememberAsyncImagePainter +import com.google.accompanist.pager.ExperimentalPagerApi +import com.google.accompanist.pager.HorizontalPager +import com.google.accompanist.pager.rememberPagerState import com.owenlejeune.tvtime.R -import com.owenlejeune.tvtime.ui.components.BackdropImage +import com.owenlejeune.tvtime.api.tmdb.api.v3.model.ImageCollection +import com.owenlejeune.tvtime.preferences.AppPreferences import com.owenlejeune.tvtime.ui.components.PosterItem import com.owenlejeune.tvtime.ui.components.RatingRing import com.owenlejeune.tvtime.utils.TmdbUtils +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import org.koin.java.KoinJavaComponent.get @Composable fun DetailHeader( modifier: Modifier = Modifier, + imageCollection: ImageCollection? = null, backdropUrl: String? = null, posterUrl: String? = null, backdropContentDescription: String? = null, @@ -47,16 +51,22 @@ fun DetailHeader( backdropImage, posterImage, ratingsView ) = createRefs() - Backdrop( - modifier = Modifier - .constrainAs(backdropImage) { - top.linkTo(parent.top) - start.linkTo(parent.start) - end.linkTo(parent.end) - }, - imageUrl = backdropUrl, - contentDescription = backdropContentDescription - ) + if (imageCollection != null) { + BackdropGallery( + modifier = Modifier + .constrainAs(backdropImage) { + top.linkTo(parent.top) + start.linkTo(parent.start) + end.linkTo(parent.end) + }, + imageCollection = imageCollection + ) + } else { + Backdrop( + imageUrl = backdropUrl, + contentDescription = backdropContentDescription + ) + } PosterItem( modifier = Modifier @@ -83,21 +93,95 @@ fun DetailHeader( } @Composable -private fun Backdrop(modifier: Modifier, imageUrl: String?, contentDescription: String? = null) { -// val images = remember { mutableStateOf(null) } -// itemId?.let { -// if (images.value == null) { -// fetchImages(itemId, service, images) -// } -// } - BackdropImage( - modifier = modifier - .fillMaxWidth() - .height(230.dp), - imageUrl = TmdbUtils.getFullBackdropPath(imageUrl), - contentDescription = contentDescription -// collection = images.value +private fun BackdropContainer( + modifier: Modifier = Modifier, + content: @Composable (MutableState) -> Unit +) { + val sizeImage = remember { mutableStateOf(IntSize.Zero) } + + val gradient = Brush.verticalGradient( + colors = listOf(Color.Transparent, MaterialTheme.colorScheme.background), + startY = sizeImage.value.height.toFloat() / 3, + endY = sizeImage.value.height.toFloat() ) + + Box( + modifier = modifier + ) { + content(sizeImage) + + Box( + modifier = Modifier + .matchParentSize() + .background(gradient) + ) + } +} + +@Composable +private fun Backdrop( + modifier: Modifier = Modifier, + imageUrl: String?, + contentDescription: String? = null +) { + BackdropContainer( + modifier = modifier + ) { sizeImage -> + if (imageUrl != null) { + AsyncImage( + model = imageUrl, + placeholder = rememberAsyncImagePainter(model = R.drawable.placeholder), + contentDescription = contentDescription, + modifier = Modifier.onGloballyPositioned { sizeImage.value = it.size }, + contentScale = ContentScale.FillWidth + ) + } else { + Image( + painter = rememberAsyncImagePainter(model = R.drawable.placeholder), + contentDescription = contentDescription, + modifier = Modifier.onGloballyPositioned { sizeImage.value = it.size }, + contentScale = ContentScale.FillWidth + ) + } + } +} + +@OptIn(ExperimentalPagerApi::class) +@Composable +private fun BackdropGallery( + modifier: Modifier, + imageCollection: ImageCollection? +) { + BackdropContainer( + modifier = modifier + ) { sizeImage -> + if (imageCollection != null) { + val pagerState = rememberPagerState() + HorizontalPager(count = imageCollection.backdrops.size, state = pagerState) { page -> + val backdrop = imageCollection.backdrops[page] + AsyncImage( + model = TmdbUtils.getFullBackdropPath(backdrop), + placeholder = rememberAsyncImagePainter(model = R.drawable.placeholder), + contentDescription = "", + modifier = Modifier.onGloballyPositioned { sizeImage.value = it.size } + ) + } + + LaunchedEffect(key1 = pagerState.currentPage) { + launch { + delay(5000) + with (pagerState) { + val target = if (currentPage < pageCount - 1) currentPage + 1 else 0 + + animateScrollToPage( + page = target, + pageOffset = 0f + ) + } + } + } + } + } } @Composable diff --git a/app/src/main/java/com/owenlejeune/tvtime/ui/screens/main/MediaDetailView.kt b/app/src/main/java/com/owenlejeune/tvtime/ui/screens/main/MediaDetailView.kt index 4510e6b..3327d83 100644 --- a/app/src/main/java/com/owenlejeune/tvtime/ui/screens/main/MediaDetailView.kt +++ b/app/src/main/java/com/owenlejeune/tvtime/ui/screens/main/MediaDetailView.kt @@ -37,6 +37,7 @@ import com.owenlejeune.tvtime.api.tmdb.api.v3.MoviesService import com.owenlejeune.tvtime.api.tmdb.api.v3.TvService import com.owenlejeune.tvtime.api.tmdb.api.v3.model.* import com.owenlejeune.tvtime.extensions.listItems +import com.owenlejeune.tvtime.preferences.AppPreferences import com.owenlejeune.tvtime.ui.components.* import com.owenlejeune.tvtime.ui.navigation.MainNavItem import com.owenlejeune.tvtime.ui.theme.FavoriteSelected @@ -47,6 +48,7 @@ import com.owenlejeune.tvtime.utils.SessionManager import com.owenlejeune.tvtime.utils.TmdbUtils import kotlinx.coroutines.* import org.json.JSONObject +import org.koin.java.KoinJavaComponent import java.text.DecimalFormat @OptIn(ExperimentalMaterial3Api::class) @@ -54,7 +56,8 @@ import java.text.DecimalFormat fun MediaDetailView( appNavController: NavController, itemId: Int?, - type: MediaViewType + type: MediaViewType, + preferences: AppPreferences = KoinJavaComponent.get(AppPreferences::class.java) ) { val service = when (type) { MediaViewType.MOVIE -> MoviesService() @@ -108,11 +111,19 @@ fun MediaDetailView( .padding(bottom = 16.dp), verticalArrangement = Arrangement.spacedBy(16.dp) ) { + val images = remember { mutableStateOf(null) } + itemId?.let { + if (preferences.showBackdropGallery && images.value == null) { + fetchImages(itemId, service, images) + } + } + DetailHeader( posterUrl = TmdbUtils.getFullPosterPath(mediaItem.value?.posterPath), posterContentDescription = mediaItem.value?.title, backdropUrl = TmdbUtils.getFullBackdropPath(mediaItem.value?.backdropPath), - rating = mediaItem.value?.voteAverage?.let { it / 10 } + rating = mediaItem.value?.voteAverage?.let { it / 10 }, + imageCollection = images.value ) Column( @@ -995,7 +1006,7 @@ private fun fetchMediaItem(id: Int, service: DetailService, mediaItem: MutableSt } } -private fun fetchImages(id: Int, service: DetailService, images: MutableState) { +fun fetchImages(id: Int, service: DetailService, images: MutableState) { CoroutineScope(Dispatchers.IO).launch { val response = service.getImages(id) if (response.isSuccessful) {