mirror of
https://github.com/owenlejeune/TVTime.git
synced 2025-11-23 12:10:59 -05:00
backdrop gallery overlay on tap
This commit is contained in:
@@ -1,7 +1,14 @@
|
||||
package com.owenlejeune.tvtime.extensions
|
||||
|
||||
import android.content.Context
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
fun Float.dpToPx(context: Context): Float {
|
||||
return this * context.resources.displayMetrics.density
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun Int.toDp(): Dp = (this / LocalContext.current.resources.displayMetrics.density).toInt().dp
|
||||
|
||||
@@ -0,0 +1,273 @@
|
||||
package com.owenlejeune.tvtime.ui.components
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.layout.wrapContentHeight
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.ChevronLeft
|
||||
import androidx.compose.material.icons.outlined.ChevronRight
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
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.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.TileMode
|
||||
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 coil.compose.AsyncImage
|
||||
import com.google.accompanist.pager.ExperimentalPagerApi
|
||||
import com.google.accompanist.pager.HorizontalPager
|
||||
import com.google.accompanist.pager.PagerState
|
||||
import com.owenlejeune.tvtime.extensions.toDp
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
@OptIn(ExperimentalPagerApi::class)
|
||||
@Composable
|
||||
fun Gallery(
|
||||
pagerState: PagerState,
|
||||
models: List<Any?>,
|
||||
modifier: Modifier = Modifier,
|
||||
contentDescriptions: List<String> = emptyList()
|
||||
) {
|
||||
HorizontalPager(
|
||||
count = models.size,
|
||||
state = pagerState,
|
||||
modifier = modifier
|
||||
) { page ->
|
||||
AsyncImage(
|
||||
model = models[page],
|
||||
contentDescription = contentDescriptions[page],
|
||||
contentScale = ContentScale.FillWidth
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalPagerApi::class)
|
||||
@Composable
|
||||
fun TapGallery(
|
||||
pagerState: PagerState,
|
||||
models: List<Any?>,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
var showControls by remember { mutableStateOf(false) }
|
||||
var lastTappedTime by remember { mutableStateOf(System.currentTimeMillis()) }
|
||||
|
||||
var job: Job? = null
|
||||
LaunchedEffect(lastTappedTime) {
|
||||
if (showControls) {
|
||||
job = scope.launch {
|
||||
delay(5.seconds)
|
||||
withContext(Dispatchers.Main) {
|
||||
showControls = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Box(
|
||||
modifier = modifier
|
||||
.clickable {
|
||||
job?.cancel()
|
||||
showControls = true
|
||||
lastTappedTime = System.currentTimeMillis()
|
||||
}
|
||||
) {
|
||||
val sizeImage = remember { mutableStateOf(IntSize.Zero) }
|
||||
HorizontalPager(
|
||||
count = models.size,
|
||||
state = pagerState,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.wrapContentHeight()
|
||||
.onGloballyPositioned { sizeImage.value = it.size }
|
||||
) { page ->
|
||||
AsyncImage(
|
||||
model = models[page],
|
||||
contentDescription = null,
|
||||
contentScale = ContentScale.FillWidth
|
||||
)
|
||||
}
|
||||
|
||||
AnimatedVisibility(
|
||||
visible = showControls,
|
||||
enter = fadeIn(),
|
||||
exit = fadeOut()
|
||||
) {
|
||||
val gradient = Brush.horizontalGradient(
|
||||
colors = listOf(Color.Black, Color.Transparent),
|
||||
startX = 0f,
|
||||
endX = (sizeImage.value.width/2).toFloat(),
|
||||
tileMode = TileMode.Mirror
|
||||
)
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.background(brush = gradient)
|
||||
.size(
|
||||
width = sizeImage.value.width.toDp(),
|
||||
height = sizeImage.value.height.toDp()
|
||||
)
|
||||
) {
|
||||
IconButton(
|
||||
modifier = Modifier
|
||||
.fillMaxHeight()
|
||||
.width(width = 48.dp)
|
||||
.align(Alignment.CenterStart),
|
||||
onClick = {}
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.ChevronLeft,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.align(Alignment.Center),
|
||||
tint = MaterialTheme.colorScheme.surface
|
||||
)
|
||||
}
|
||||
|
||||
IconButton(
|
||||
modifier = Modifier
|
||||
.fillMaxHeight()
|
||||
.width(width = 48.dp)
|
||||
.align(Alignment.CenterEnd),
|
||||
onClick = {}
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.ChevronRight,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.align(Alignment.Center),
|
||||
tint = MaterialTheme.colorScheme.surface
|
||||
)
|
||||
}
|
||||
// Box(
|
||||
// modifier = Modifier
|
||||
// .background(brush = leftGradient)
|
||||
// .height(height = sizeImage.value.height.toDp())
|
||||
// .width((sizeImage.value.width/2).toDp())
|
||||
// .align(Alignment.CenterStart)
|
||||
// .onGloballyPositioned { leftSizeImage.value = it.size }
|
||||
// .clickable {
|
||||
// val target =
|
||||
// if (pagerState.currentPage == 0) models.size - 1 else pagerState.currentPage - 1
|
||||
// scope.launch { pagerState.animateScrollToPage(target) }
|
||||
// }
|
||||
// ) {
|
||||
// Icon(
|
||||
// imageVector = Icons.Outlined.ChevronLeft,
|
||||
// contentDescription = null,
|
||||
// modifier = Modifier
|
||||
// .size(48.dp)
|
||||
// .padding(start = 24.dp)
|
||||
// .align(Alignment.CenterStart),
|
||||
// tint = MaterialTheme.colorScheme.surface
|
||||
// )
|
||||
// }
|
||||
// Box(
|
||||
// modifier = Modifier
|
||||
// .background(brush = rightGradient)
|
||||
// .height(height = sizeImage.value.height.toDp())
|
||||
// .width((sizeImage.value.width/2).toDp())
|
||||
// .align(Alignment.CenterEnd)
|
||||
// .onGloballyPositioned { rightSizeImage.value = it.size }
|
||||
// .clickable {
|
||||
// val target =
|
||||
// if (pagerState.currentPage == models.size - 1) 0 else pagerState.currentPage + 1
|
||||
// scope.launch { pagerState.animateScrollToPage(target) }
|
||||
// }
|
||||
// ) {
|
||||
// Icon(
|
||||
// imageVector = Icons.Outlined.ChevronRight,
|
||||
// contentDescription = null,
|
||||
// modifier = Modifier
|
||||
// .size(48.dp)
|
||||
// .padding(end = 24.dp)
|
||||
// .align(Alignment.CenterEnd),
|
||||
// tint = MaterialTheme.colorScheme.surface
|
||||
// )
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
// AnimatedVisibility(
|
||||
// visible = showControls.value,
|
||||
// enter = fadeIn(),
|
||||
// exit = fadeOut()
|
||||
// ) {
|
||||
// val leftSizeImage = remember { mutableStateOf(IntSize.Zero) }
|
||||
// val leftGradient = Brush.horizontalGradient(
|
||||
// colors = listOf(Color.Black, Color.Transparent),
|
||||
// startX = 0f,
|
||||
// endX = leftSizeImage.value.width.toFloat()
|
||||
// )
|
||||
// Box(
|
||||
// modifier = Modifier
|
||||
// .background(brush = leftGradient)
|
||||
// .fillMaxHeight()
|
||||
// .width(100.dp)
|
||||
// .align(Alignment.CenterStart)
|
||||
// .onGloballyPositioned { leftSizeImage.value = it.size }
|
||||
// .clickable {
|
||||
// val target =
|
||||
// if (pagerState.currentPage == 0) models.size - 1 else pagerState.currentPage + 1
|
||||
// scope.launch { pagerState.animateScrollToPage(target) }
|
||||
// }
|
||||
// ) {
|
||||
// Icon(
|
||||
// imageVector = Icons.Outlined.ChevronLeft,
|
||||
// contentDescription = null,
|
||||
// modifier = Modifier.size(48.dp)
|
||||
// )
|
||||
// }
|
||||
//
|
||||
// val rightSizeImage = remember { mutableStateOf(IntSize.Zero) }
|
||||
// val rightGradient = Brush.horizontalGradient(
|
||||
// colors = listOf(Color.Black, Color.Transparent),
|
||||
// startX = leftSizeImage.value.width.toFloat(),
|
||||
// endX = 0f
|
||||
// )
|
||||
// Box(
|
||||
// modifier = Modifier
|
||||
// .background(brush = rightGradient)
|
||||
// .fillMaxHeight()
|
||||
// .width(100.dp)
|
||||
// .align(Alignment.CenterEnd)
|
||||
// .onGloballyPositioned { rightSizeImage.value = it.size }
|
||||
// .clickable {
|
||||
// val target =
|
||||
// if (pagerState.currentPage == models.size - 1) 0 else pagerState.currentPage - 1
|
||||
// scope.launch { pagerState.animateScrollToPage(target) }
|
||||
// }
|
||||
// ) {
|
||||
// Icon(
|
||||
// imageVector = Icons.Outlined.ChevronRight,
|
||||
// contentDescription = null,
|
||||
// modifier = Modifier.size(48.dp)
|
||||
// )
|
||||
// }
|
||||
// }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package com.owenlejeune.tvtime.ui.components
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.wrapContentHeight
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import com.google.accompanist.pager.ExperimentalPagerApi
|
||||
import com.google.accompanist.pager.rememberPagerState
|
||||
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.ImageCollection
|
||||
import com.owenlejeune.tvtime.utils.TmdbUtils
|
||||
|
||||
@OptIn(ExperimentalPagerApi::class)
|
||||
@Composable
|
||||
fun ImageGalleryOverlay(
|
||||
imageCollection: ImageCollection,
|
||||
selectedImage: Int,
|
||||
onDismissRequest: () -> Unit
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(color = Color.Black.copy(alpha = 0.7f))
|
||||
.clickable(onClick = onDismissRequest)
|
||||
) {
|
||||
val pagerState = rememberPagerState(initialPage = selectedImage)
|
||||
TapGallery(
|
||||
pagerState = pagerState,
|
||||
models = imageCollection.backdrops.map { TmdbUtils.getFullBackdropPath(it) },
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.wrapContentHeight()
|
||||
.align(Alignment.Center)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package com.owenlejeune.tvtime.ui.screens.main
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
@@ -20,6 +21,7 @@ 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.PagerState
|
||||
import com.google.accompanist.pager.rememberPagerState
|
||||
import com.owenlejeune.tvtime.R
|
||||
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.ImageCollection
|
||||
@@ -31,15 +33,18 @@ import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
@OptIn(ExperimentalPagerApi::class)
|
||||
@Composable
|
||||
fun DetailHeader(
|
||||
modifier: Modifier = Modifier,
|
||||
showGalleryOverlay: MutableState<Boolean>? = null,
|
||||
imageCollection: ImageCollection? = null,
|
||||
backdropUrl: String? = null,
|
||||
posterUrl: String? = null,
|
||||
backdropContentDescription: String? = null,
|
||||
posterContentDescription: String? = null,
|
||||
rating: Float? = null
|
||||
rating: Float? = null,
|
||||
pagerState: PagerState? = null
|
||||
) {
|
||||
ConstraintLayout(modifier = modifier
|
||||
.fillMaxWidth()
|
||||
@@ -56,8 +61,12 @@ fun DetailHeader(
|
||||
top.linkTo(parent.top)
|
||||
start.linkTo(parent.start)
|
||||
end.linkTo(parent.end)
|
||||
}
|
||||
.clickable {
|
||||
showGalleryOverlay?.value = true
|
||||
},
|
||||
imageCollection = imageCollection
|
||||
imageCollection = imageCollection,
|
||||
state = pagerState
|
||||
)
|
||||
} else {
|
||||
Backdrop(
|
||||
@@ -148,15 +157,17 @@ private fun Backdrop(
|
||||
|
||||
@OptIn(ExperimentalPagerApi::class)
|
||||
@Composable
|
||||
private fun BackdropGallery(
|
||||
fun BackdropGallery(
|
||||
modifier: Modifier,
|
||||
imageCollection: ImageCollection?
|
||||
imageCollection: ImageCollection?,
|
||||
delayMillis: Long = 5000,
|
||||
state: PagerState? = null
|
||||
) {
|
||||
BackdropContainer(
|
||||
modifier = modifier
|
||||
) { sizeImage ->
|
||||
if (imageCollection != null) {
|
||||
val pagerState = rememberPagerState()
|
||||
val pagerState = state ?: rememberPagerState(initialPage = 0)
|
||||
HorizontalPager(
|
||||
count = imageCollection.backdrops.size,
|
||||
state = pagerState,
|
||||
@@ -173,14 +184,16 @@ private fun BackdropGallery(
|
||||
}
|
||||
|
||||
// fixes an issue where using pagerState.current page breaks paging animations
|
||||
var key by remember { mutableStateOf(false) }
|
||||
LaunchedEffect(key1 = key) {
|
||||
launch {
|
||||
delay(5000)
|
||||
with(pagerState) {
|
||||
val target = if (currentPage < pageCount - 1) currentPage + 1 else 0
|
||||
animateScrollToPage(target)
|
||||
key = !key
|
||||
if (delayMillis > 0) {
|
||||
var key by remember { mutableStateOf(false) }
|
||||
LaunchedEffect(key1 = key) {
|
||||
launch {
|
||||
delay(delayMillis)
|
||||
with(pagerState) {
|
||||
val target = if (currentPage < pageCount - 1) currentPage + 1 else 0
|
||||
animateScrollToPage(target)
|
||||
key = !key
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.owenlejeune.tvtime.ui.screens.main
|
||||
|
||||
import android.content.Context
|
||||
import android.media.MediaActionSound
|
||||
import android.widget.Toast
|
||||
import androidx.compose.animation.*
|
||||
import androidx.compose.animation.core.tween
|
||||
@@ -31,6 +32,9 @@ import androidx.compose.ui.unit.DpSize
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.navigation.NavController
|
||||
import com.google.accompanist.pager.ExperimentalPagerApi
|
||||
import com.google.accompanist.pager.PagerState
|
||||
import com.google.accompanist.pager.rememberPagerState
|
||||
import com.owenlejeune.tvtime.R
|
||||
import com.owenlejeune.tvtime.api.tmdb.api.v3.AccountService
|
||||
import com.owenlejeune.tvtime.api.tmdb.api.v3.DetailService
|
||||
@@ -51,16 +55,17 @@ import com.owenlejeune.tvtime.utils.TmdbUtils
|
||||
import kotlinx.coroutines.*
|
||||
import org.json.JSONObject
|
||||
import org.koin.java.KoinJavaComponent
|
||||
import org.koin.java.KoinJavaComponent.get
|
||||
import java.text.DecimalFormat
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalPagerApi::class)
|
||||
@Composable
|
||||
fun MediaDetailView(
|
||||
appNavController: NavController,
|
||||
itemId: Int?,
|
||||
type: MediaViewType,
|
||||
windowSize: WindowSizeClass,
|
||||
preferences: AppPreferences = KoinJavaComponent.get(AppPreferences::class.java)
|
||||
preferences: AppPreferences = get(AppPreferences::class.java)
|
||||
) {
|
||||
val service = when (type) {
|
||||
MediaViewType.MOVIE -> MoviesService()
|
||||
@@ -75,108 +80,153 @@ fun MediaDetailView(
|
||||
}
|
||||
}
|
||||
|
||||
val images = remember { mutableStateOf<ImageCollection?>(null) }
|
||||
itemId?.let {
|
||||
if (preferences.showBackdropGallery && images.value == null) {
|
||||
fetchImages(itemId, service, images)
|
||||
}
|
||||
}
|
||||
|
||||
val decayAnimationSpec = rememberSplineBasedDecay<Float>()
|
||||
val topAppBarScrollState = rememberTopAppBarScrollState()
|
||||
val scrollBehavior = remember(decayAnimationSpec) {
|
||||
TopAppBarDefaults.pinnedScrollBehavior(topAppBarScrollState)
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
|
||||
topBar = {
|
||||
SmallTopAppBar(
|
||||
scrollBehavior = scrollBehavior,
|
||||
colors = TopAppBarDefaults
|
||||
.smallTopAppBarColors(
|
||||
scrolledContainerColor = MaterialTheme.colorScheme.background,
|
||||
titleContentColor = MaterialTheme.colorScheme.primary
|
||||
),
|
||||
title = { Text(text = mediaItem.value?.title ?: "") },
|
||||
navigationIcon = {
|
||||
IconButton(
|
||||
onClick = { appNavController.popBackStack() }
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.ArrowBack,
|
||||
contentDescription = stringResource(id = R.string.content_description_back_button),
|
||||
tint = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
val pagerState = rememberPagerState(initialPage = 0)
|
||||
|
||||
Box(
|
||||
modifier = Modifier.fillMaxSize()
|
||||
) {
|
||||
val showGalleryOverlay = remember { mutableStateOf(false) }
|
||||
Scaffold(
|
||||
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
|
||||
topBar = {
|
||||
SmallTopAppBar(
|
||||
scrollBehavior = scrollBehavior,
|
||||
colors = TopAppBarDefaults
|
||||
.smallTopAppBarColors(
|
||||
scrolledContainerColor = MaterialTheme.colorScheme.background,
|
||||
titleContentColor = MaterialTheme.colorScheme.primary
|
||||
),
|
||||
title = { Text(text = mediaItem.value?.title ?: "") },
|
||||
navigationIcon = {
|
||||
IconButton(
|
||||
onClick = { appNavController.popBackStack() }
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.ArrowBack,
|
||||
contentDescription = stringResource(id = R.string.content_description_back_button),
|
||||
tint = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
) { innerPadding ->
|
||||
Box(modifier = Modifier.padding(innerPadding)) {
|
||||
MediaViewContent(
|
||||
appNavController = appNavController,
|
||||
itemId = itemId,
|
||||
mediaItem = mediaItem,
|
||||
images = images,
|
||||
service = service,
|
||||
type = type,
|
||||
windowSize = windowSize,
|
||||
showImageGallery = showGalleryOverlay,
|
||||
pagerState = pagerState
|
||||
)
|
||||
}
|
||||
}
|
||||
) { innerPadding ->
|
||||
Box(modifier = Modifier.padding(innerPadding)) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.background(color = MaterialTheme.colorScheme.background),
|
||||
horizontalArrangement = Arrangement.spacedBy(16.dp)
|
||||
|
||||
if (showGalleryOverlay.value) {
|
||||
images.value?.let {
|
||||
ImageGalleryOverlay(
|
||||
imageCollection = it,
|
||||
selectedImage = pagerState.currentPage,
|
||||
onDismissRequest = { showGalleryOverlay.value = false }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalPagerApi::class)
|
||||
@Composable
|
||||
private fun MediaViewContent(
|
||||
appNavController: NavController,
|
||||
itemId: Int?,
|
||||
mediaItem: MutableState<DetailedItem?>,
|
||||
images: MutableState<ImageCollection?>,
|
||||
service: DetailService,
|
||||
type: MediaViewType,
|
||||
windowSize: WindowSizeClass,
|
||||
showImageGallery: MutableState<Boolean>,
|
||||
pagerState: PagerState
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.background(color = MaterialTheme.colorScheme.background),
|
||||
horizontalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.background(color = MaterialTheme.colorScheme.background)
|
||||
.weight(1f)
|
||||
.verticalScroll(state = rememberScrollState()),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
DetailHeader(
|
||||
posterUrl = TmdbUtils.getFullPosterPath(mediaItem.value?.posterPath),
|
||||
posterContentDescription = mediaItem.value?.title,
|
||||
backdropUrl = TmdbUtils.getFullBackdropPath(mediaItem.value?.backdropPath),
|
||||
rating = mediaItem.value?.voteAverage?.let { it / 10 },
|
||||
imageCollection = images.value,
|
||||
showGalleryOverlay = showImageGallery,
|
||||
pagerState = pagerState
|
||||
)
|
||||
|
||||
Column(
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.background(color = MaterialTheme.colorScheme.background)
|
||||
.weight(1f)
|
||||
.verticalScroll(state = rememberScrollState()),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
val images = remember { mutableStateOf<ImageCollection?>(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 },
|
||||
imageCollection = images.value
|
||||
)
|
||||
|
||||
Column(
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
if (type == MediaViewType.MOVIE) {
|
||||
MiscMovieDetails(mediaItem = mediaItem, service as MoviesService)
|
||||
} else {
|
||||
MiscTvDetails(mediaItem = mediaItem, service as TvService)
|
||||
}
|
||||
|
||||
ActionsView(itemId = itemId, type = type, service = service)
|
||||
|
||||
OverviewCard(itemId = itemId, mediaItem = mediaItem, service = service)
|
||||
|
||||
CastCard(itemId = itemId, service = service, appNavController = appNavController)
|
||||
|
||||
SimilarContentCard(itemId = itemId, service = service, mediaType = type, appNavController = appNavController)
|
||||
|
||||
VideosCard(itemId = itemId, service = service)
|
||||
|
||||
AdditionalDetailsCard(itemId = itemId, mediaItem = mediaItem, service = service, type = type)
|
||||
|
||||
if (windowSize != WindowSizeClass.Expanded) {
|
||||
ReviewsCard(itemId = itemId, service = service)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
if (type == MediaViewType.MOVIE) {
|
||||
MiscMovieDetails(mediaItem = mediaItem, service as MoviesService)
|
||||
} else {
|
||||
MiscTvDetails(mediaItem = mediaItem, service as TvService)
|
||||
}
|
||||
|
||||
if (windowSize == WindowSizeClass.Expanded) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.background(color = MaterialTheme.colorScheme.background)
|
||||
.weight(1f)
|
||||
.verticalScroll(state = rememberScrollState())
|
||||
) {
|
||||
ReviewsCard(itemId = itemId, service = service)
|
||||
ActionsView(itemId = itemId, type = type, service = service)
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
}
|
||||
OverviewCard(itemId = itemId, mediaItem = mediaItem, service = service)
|
||||
|
||||
CastCard(itemId = itemId, service = service, appNavController = appNavController)
|
||||
|
||||
SimilarContentCard(itemId = itemId, service = service, mediaType = type, appNavController = appNavController)
|
||||
|
||||
VideosCard(itemId = itemId, service = service)
|
||||
|
||||
AdditionalDetailsCard(itemId = itemId, mediaItem = mediaItem, service = service, type = type)
|
||||
|
||||
if (windowSize != WindowSizeClass.Expanded) {
|
||||
ReviewsCard(itemId = itemId, service = service)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
}
|
||||
|
||||
if (windowSize == WindowSizeClass.Expanded) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.background(color = MaterialTheme.colorScheme.background)
|
||||
.weight(1f)
|
||||
.verticalScroll(state = rememberScrollState())
|
||||
) {
|
||||
ReviewsCard(itemId = itemId, service = service)
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -616,55 +666,57 @@ private fun OverviewCard(
|
||||
}
|
||||
|
||||
mediaItem.value?.let { mi ->
|
||||
ContentCard(
|
||||
modifier = modifier
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.wrapContentHeight()
|
||||
.padding(vertical = 12.dp, horizontal = 16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
if (!mi.tagline.isNullOrEmpty() || keywordResponse.value?.keywords?.isNotEmpty() == true || !mi.overview.isNullOrEmpty()) {
|
||||
ContentCard(
|
||||
modifier = modifier
|
||||
) {
|
||||
mi.tagline?.let { tagline ->
|
||||
if (tagline.isNotEmpty()) {
|
||||
Text(
|
||||
text = tagline,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
fontStyle = FontStyle.Italic,
|
||||
)
|
||||
}
|
||||
}
|
||||
Text(
|
||||
text = mi.overview ?: "",
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
|
||||
|
||||
keywordResponse.value?.keywords?.let { keywords ->
|
||||
val keywordsChipInfo = keywords.map { ChipInfo(it.name, false) }
|
||||
Row(
|
||||
modifier = Modifier.horizontalScroll(rememberScrollState()),
|
||||
horizontalArrangement = Arrangement.spacedBy(ChipStyle.Rounded.mainAxisSpacing)
|
||||
) {
|
||||
keywordsChipInfo.forEach { keywordChipInfo ->
|
||||
RoundedChip(
|
||||
text = keywordChipInfo.text,
|
||||
enabled = keywordChipInfo.enabled,
|
||||
colors = ChipDefaults.roundedChipColors(
|
||||
unselectedContainerColor = MaterialTheme.colorScheme.primary,
|
||||
unselectedContentColor = MaterialTheme.colorScheme.primary
|
||||
),
|
||||
onSelectionChanged = { chip ->
|
||||
if (service is MoviesService) {
|
||||
// Toast.makeText(context, chip, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.wrapContentHeight()
|
||||
.padding(vertical = 12.dp, horizontal = 16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
mi.tagline?.let { tagline ->
|
||||
if (tagline.isNotEmpty()) {
|
||||
Text(
|
||||
text = tagline,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
fontStyle = FontStyle.Italic,
|
||||
)
|
||||
}
|
||||
}
|
||||
Text(
|
||||
text = mi.overview ?: "",
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
|
||||
|
||||
keywordResponse.value?.keywords?.let { keywords ->
|
||||
val keywordsChipInfo = keywords.map { ChipInfo(it.name, false) }
|
||||
Row(
|
||||
modifier = Modifier.horizontalScroll(rememberScrollState()),
|
||||
horizontalArrangement = Arrangement.spacedBy(ChipStyle.Rounded.mainAxisSpacing)
|
||||
) {
|
||||
keywordsChipInfo.forEach { keywordChipInfo ->
|
||||
RoundedChip(
|
||||
text = keywordChipInfo.text,
|
||||
enabled = keywordChipInfo.enabled,
|
||||
colors = ChipDefaults.roundedChipColors(
|
||||
unselectedContainerColor = MaterialTheme.colorScheme.primary,
|
||||
unselectedContentColor = MaterialTheme.colorScheme.primary
|
||||
),
|
||||
onSelectionChanged = { chip ->
|
||||
if (service is MoviesService) {
|
||||
// Toast.makeText(context, chip, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.navigation.NavController
|
||||
import com.google.accompanist.pager.ExperimentalPagerApi
|
||||
import com.owenlejeune.tvtime.R
|
||||
import com.owenlejeune.tvtime.api.tmdb.api.v3.PeopleService
|
||||
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.DetailPerson
|
||||
@@ -33,7 +34,7 @@ import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalPagerApi::class)
|
||||
@Composable
|
||||
fun PersonDetailView(
|
||||
appNavController: NavController,
|
||||
|
||||
Reference in New Issue
Block a user