refactor support for paging backdrop images

This commit is contained in:
Owen LeJeune
2022-08-31 23:25:53 -04:00
parent 8ef1d05965
commit ec12621743
6 changed files with 160 additions and 124 deletions

View File

@@ -77,15 +77,20 @@ class TmdbClient: KoinComponent {
override fun intercept(chain: Interceptor.Chain): Response { override fun intercept(chain: Interceptor.Chain): Response {
val apiParam = QueryParam("api_key", BuildConfig.TMDB_ApiKey) 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 segments = chain.request().url.encodedPathSegments
val sessionIdParam: QueryParam? = sessionIdParam(segments) val sessionIdParam: QueryParam? = sessionIdParam(segments)
val builder = chain.request().url.newBuilder() 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 requestBuilder = chain.request().newBuilder().url(builder.build())
val request = requestBuilder.build() val request = requestBuilder.build()
@@ -106,6 +111,16 @@ class TmdbClient: KoinComponent {
} }
return sessionIdParam return sessionIdParam
} }
private fun shouldIncludeLanguageParam(urlSegments: List<String>): Boolean {
val ignoredRoutes = listOf("images")
for (route in ignoredRoutes) {
if (urlSegments.contains(route)) {
return false
}
}
return true
}
} }
private inner class V4Interceptor: Interceptor { private inner class V4Interceptor: Interceptor {

View File

@@ -4,25 +4,6 @@ import com.owenlejeune.tvtime.api.QueryParam
import okhttp3.HttpUrl import okhttp3.HttpUrl
import okhttp3.Request 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 { fun HttpUrl.Builder.addQueryParams(vararg queryParams: QueryParam?): HttpUrl.Builder {
return apply { return apply {
queryParams.forEach { param -> queryParams.forEach { param ->

View File

@@ -6,6 +6,7 @@ import com.google.gson.Gson
import com.kieronquinn.monetcompat.core.MonetCompat import com.kieronquinn.monetcompat.core.MonetCompat
import com.owenlejeune.tvtime.BuildConfig import com.owenlejeune.tvtime.BuildConfig
import com.owenlejeune.tvtime.utils.SessionManager import com.owenlejeune.tvtime.utils.SessionManager
import java.util.function.BiPredicate
class AppPreferences(context: Context) { class AppPreferences(context: Context) {
@@ -24,6 +25,7 @@ class AppPreferences(context: Context) {
private val USE_SYSTEM_COLORS = "use_system_colors" private val USE_SYSTEM_COLORS = "use_system_colors"
private val SELECTED_COLOR = "selected_color" private val SELECTED_COLOR = "selected_color"
private val USE_V4_API = "use_v4_api" 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) 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) get() = preferences.getBoolean(USE_V4_API, true)
set(value) { preferences.put(USE_V4_API, value) } set(value) { preferences.put(USE_V4_API, value) }
var showBackdropGallery: Boolean = true
private fun SharedPreferences.put(key: String, value: Any?) { private fun SharedPreferences.put(key: String, value: Any?) {
edit().apply { edit().apply {
when (value) { when (value) {

View File

@@ -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)
)
}
} }

View File

@@ -1,38 +1,42 @@
package com.owenlejeune.tvtime.ui.screens.main 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.background
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.CircleShape 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.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.runtime.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import androidx.constraintlayout.compose.ConstraintLayout 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.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.PosterItem
import com.owenlejeune.tvtime.ui.components.RatingRing import com.owenlejeune.tvtime.ui.components.RatingRing
import com.owenlejeune.tvtime.utils.TmdbUtils import com.owenlejeune.tvtime.utils.TmdbUtils
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import org.koin.java.KoinJavaComponent.get
@Composable @Composable
fun DetailHeader( fun DetailHeader(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
imageCollection: ImageCollection? = null,
backdropUrl: String? = null, backdropUrl: String? = null,
posterUrl: String? = null, posterUrl: String? = null,
backdropContentDescription: String? = null, backdropContentDescription: String? = null,
@@ -47,16 +51,22 @@ fun DetailHeader(
backdropImage, posterImage, ratingsView backdropImage, posterImage, ratingsView
) = createRefs() ) = createRefs()
Backdrop( if (imageCollection != null) {
modifier = Modifier BackdropGallery(
.constrainAs(backdropImage) { modifier = Modifier
top.linkTo(parent.top) .constrainAs(backdropImage) {
start.linkTo(parent.start) top.linkTo(parent.top)
end.linkTo(parent.end) start.linkTo(parent.start)
}, end.linkTo(parent.end)
imageUrl = backdropUrl, },
contentDescription = backdropContentDescription imageCollection = imageCollection
) )
} else {
Backdrop(
imageUrl = backdropUrl,
contentDescription = backdropContentDescription
)
}
PosterItem( PosterItem(
modifier = Modifier modifier = Modifier
@@ -83,21 +93,95 @@ fun DetailHeader(
} }
@Composable @Composable
private fun Backdrop(modifier: Modifier, imageUrl: String?, contentDescription: String? = null) { private fun BackdropContainer(
// val images = remember { mutableStateOf<ImageCollection?>(null) } modifier: Modifier = Modifier,
// itemId?.let { content: @Composable (MutableState<IntSize>) -> Unit
// if (images.value == null) { ) {
// fetchImages(itemId, service, images) val sizeImage = remember { mutableStateOf(IntSize.Zero) }
// }
// } val gradient = Brush.verticalGradient(
BackdropImage( colors = listOf(Color.Transparent, MaterialTheme.colorScheme.background),
modifier = modifier startY = sizeImage.value.height.toFloat() / 3,
.fillMaxWidth() endY = sizeImage.value.height.toFloat()
.height(230.dp),
imageUrl = TmdbUtils.getFullBackdropPath(imageUrl),
contentDescription = contentDescription
// collection = images.value
) )
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 @Composable

View File

@@ -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.TvService
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.* import com.owenlejeune.tvtime.api.tmdb.api.v3.model.*
import com.owenlejeune.tvtime.extensions.listItems import com.owenlejeune.tvtime.extensions.listItems
import com.owenlejeune.tvtime.preferences.AppPreferences
import com.owenlejeune.tvtime.ui.components.* import com.owenlejeune.tvtime.ui.components.*
import com.owenlejeune.tvtime.ui.navigation.MainNavItem import com.owenlejeune.tvtime.ui.navigation.MainNavItem
import com.owenlejeune.tvtime.ui.theme.FavoriteSelected import com.owenlejeune.tvtime.ui.theme.FavoriteSelected
@@ -47,6 +48,7 @@ import com.owenlejeune.tvtime.utils.SessionManager
import com.owenlejeune.tvtime.utils.TmdbUtils import com.owenlejeune.tvtime.utils.TmdbUtils
import kotlinx.coroutines.* import kotlinx.coroutines.*
import org.json.JSONObject import org.json.JSONObject
import org.koin.java.KoinJavaComponent
import java.text.DecimalFormat import java.text.DecimalFormat
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@@ -54,7 +56,8 @@ import java.text.DecimalFormat
fun MediaDetailView( fun MediaDetailView(
appNavController: NavController, appNavController: NavController,
itemId: Int?, itemId: Int?,
type: MediaViewType type: MediaViewType,
preferences: AppPreferences = KoinJavaComponent.get(AppPreferences::class.java)
) { ) {
val service = when (type) { val service = when (type) {
MediaViewType.MOVIE -> MoviesService() MediaViewType.MOVIE -> MoviesService()
@@ -108,11 +111,19 @@ fun MediaDetailView(
.padding(bottom = 16.dp), .padding(bottom = 16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp) verticalArrangement = Arrangement.spacedBy(16.dp)
) { ) {
val images = remember { mutableStateOf<ImageCollection?>(null) }
itemId?.let {
if (preferences.showBackdropGallery && images.value == null) {
fetchImages(itemId, service, images)
}
}
DetailHeader( DetailHeader(
posterUrl = TmdbUtils.getFullPosterPath(mediaItem.value?.posterPath), posterUrl = TmdbUtils.getFullPosterPath(mediaItem.value?.posterPath),
posterContentDescription = mediaItem.value?.title, posterContentDescription = mediaItem.value?.title,
backdropUrl = TmdbUtils.getFullBackdropPath(mediaItem.value?.backdropPath), backdropUrl = TmdbUtils.getFullBackdropPath(mediaItem.value?.backdropPath),
rating = mediaItem.value?.voteAverage?.let { it / 10 } rating = mediaItem.value?.voteAverage?.let { it / 10 },
imageCollection = images.value
) )
Column( Column(
@@ -995,7 +1006,7 @@ private fun fetchMediaItem(id: Int, service: DetailService, mediaItem: MutableSt
} }
} }
private fun fetchImages(id: Int, service: DetailService, images: MutableState<ImageCollection?>) { fun fetchImages(id: Int, service: DetailService, images: MutableState<ImageCollection?>) {
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {
val response = service.getImages(id) val response = service.getImages(id)
if (response.isSuccessful) { if (response.isSuccessful) {