diff --git a/app/src/main/java/com/owenlejeune/tvtime/extensions/ModifierExtensions.kt b/app/src/main/java/com/owenlejeune/tvtime/extensions/ModifierExtensions.kt index f07f12a..d0c1962 100644 --- a/app/src/main/java/com/owenlejeune/tvtime/extensions/ModifierExtensions.kt +++ b/app/src/main/java/com/owenlejeune/tvtime/extensions/ModifierExtensions.kt @@ -1,5 +1,6 @@ package com.owenlejeune.tvtime.extensions +import android.view.View import androidx.compose.animation.core.LinearOutSlowInEasing import androidx.compose.animation.core.RepeatMode import androidx.compose.animation.core.animateFloat @@ -7,7 +8,11 @@ import androidx.compose.animation.core.infiniteRepeatable import androidx.compose.animation.core.rememberInfiniteTransition import androidx.compose.animation.core.tween import androidx.compose.foundation.background +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.composed import androidx.compose.ui.geometry.Offset @@ -16,6 +21,10 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.RectangleShape import androidx.compose.ui.graphics.Shape import androidx.compose.ui.graphics.TileMode +import androidx.compose.ui.layout.LayoutCoordinates +import androidx.compose.ui.layout.boundsInWindow +import androidx.compose.ui.layout.onGloballyPositioned +import androidx.compose.ui.platform.LocalView fun Modifier.shimmerBackground( shape: Shape = RectangleShape, @@ -41,4 +50,40 @@ fun Modifier.shimmerBackground( tileMode = TileMode.Mirror, ) return@composed this.then(background(brush, shape)) +} + +fun Modifier.combinedOnVisibilityChange( + onVisible: () -> Unit, + onNotVisible: () -> Unit +): Modifier = composed { + val view = LocalView.current + var isVisible by remember { mutableStateOf(null) } + + LaunchedEffect(isVisible) { + when (isVisible) { + true -> onVisible() + false -> onNotVisible() + else -> {} + } + } + + onGloballyPositioned { coordinates -> + isVisible = coordinates.isCompletelyVisible(view) + } +} + +fun LayoutCoordinates.isCompletelyVisible(view: View): Boolean { + if (!isAttached) return false + // Window relative bounds of our compose root view that are visible on the screen + val globalRootRect = android.graphics.Rect() + if (!view.getGlobalVisibleRect(globalRootRect)) { + // we aren't visible at all. + return false + } + val bounds = boundsInWindow() + // Make sure we are completely in bounds. + return bounds.top >= globalRootRect.top && + bounds.left >= globalRootRect.left && + bounds.right <= globalRootRect.right && + bounds.bottom <= globalRootRect.bottom } \ No newline at end of file diff --git a/app/src/main/java/com/owenlejeune/tvtime/ui/components/DetailViewCommon.kt b/app/src/main/java/com/owenlejeune/tvtime/ui/components/DetailViewCommon.kt index a929994..1cf588a 100644 --- a/app/src/main/java/com/owenlejeune/tvtime/ui/components/DetailViewCommon.kt +++ b/app/src/main/java/com/owenlejeune/tvtime/ui/components/DetailViewCommon.kt @@ -87,7 +87,7 @@ fun DetailHeader( Row( modifier = Modifier .align(Alignment.BottomStart) - .padding(start = 16.dp), + .padding(start = 16.dp, top = 16.dp), horizontalArrangement = Arrangement.spacedBy(20.dp), verticalAlignment = Alignment.Bottom ) { @@ -119,7 +119,7 @@ private fun BackdropContainer( val gradient = Brush.verticalGradient( colors = listOf(Color.Transparent, MaterialTheme.colorScheme.background), - startY = sizeImage.value.height.toFloat() / 3, + startY = 0f,//sizeImage.value.height.toFloat() / 4, endY = sizeImage.value.height.toFloat() ) @@ -130,8 +130,8 @@ private fun BackdropContainer( Box( modifier = Modifier - .width(sizeImage.value.width.toDp()) - .height(sizeImage.value.height.toDp()) + .width(sizeImage.value.width.toDp() + 2.dp) + .height(sizeImage.value.height.toDp() + 4.dp) .background(gradient) ) } @@ -187,7 +187,10 @@ fun BackdropGallery( HorizontalPager( count = imageCollection.backdrops.size, state = pagerState, - modifier = Modifier.fillMaxWidth() + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight() + .onGloballyPositioned { sizeImage.value = it.size }, ) { page -> val backdrop = imageCollection.backdrops[page] val url = TmdbUtils.getFullBackdropPath(backdrop) @@ -201,7 +204,6 @@ fun BackdropGallery( model = model, placeholder = rememberAsyncImagePainter(model = R.drawable.placeholder), contentDescription = "", - modifier = Modifier.onGloballyPositioned { sizeImage.value = it.size }, contentScale = ContentScale.FillWidth ) } diff --git a/app/src/main/java/com/owenlejeune/tvtime/ui/screens/MediaDetailScreen.kt b/app/src/main/java/com/owenlejeune/tvtime/ui/screens/MediaDetailScreen.kt index 1b4fbd0..adf0c36 100644 --- a/app/src/main/java/com/owenlejeune/tvtime/ui/screens/MediaDetailScreen.kt +++ b/app/src/main/java/com/owenlejeune/tvtime/ui/screens/MediaDetailScreen.kt @@ -4,7 +4,10 @@ import android.annotation.SuppressLint import android.content.Intent import android.net.Uri import android.widget.Toast +import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.Crossfade +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.horizontalScroll @@ -87,6 +90,7 @@ import com.owenlejeune.tvtime.api.tmdb.api.v3.model.Video import com.owenlejeune.tvtime.api.tmdb.api.v3.model.WatchProviderDetails import com.owenlejeune.tvtime.extensions.DateFormat import com.owenlejeune.tvtime.extensions.WindowSizeClass +import com.owenlejeune.tvtime.extensions.combinedOnVisibilityChange import com.owenlejeune.tvtime.extensions.format import com.owenlejeune.tvtime.extensions.getCalendarYear import com.owenlejeune.tvtime.extensions.isIn @@ -119,6 +123,7 @@ import com.owenlejeune.tvtime.ui.components.RoundedTextField import com.owenlejeune.tvtime.ui.components.TVTTopAppBar import com.owenlejeune.tvtime.ui.components.TwoLineImageTextCard import com.owenlejeune.tvtime.ui.navigation.AppNavItem +import com.owenlejeune.tvtime.ui.theme.Typography import com.owenlejeune.tvtime.ui.viewmodel.ApplicationViewModel import com.owenlejeune.tvtime.ui.viewmodel.MainViewModel import com.owenlejeune.tvtime.ui.viewmodel.SpecialFeaturesViewModel @@ -208,6 +213,7 @@ fun MediaDetailScreen( } ) + val titleViewHidden = remember { mutableStateOf(false) } Box( modifier = Modifier.fillMaxSize() ) { @@ -217,7 +223,15 @@ fun MediaDetailScreen( topBar = { TVTTopAppBar( scrollBehavior = scrollBehavior, - title = { Text(text = mediaItem?.title ?: "") }, + title = { + AnimatedVisibility( + visible = titleViewHidden.value, + enter = fadeIn(), + exit = fadeOut() + ) { + Text(text = mediaItem?.title ?: "") + } + }, appNavController = appNavController, navigationIcon = { BackButton(navController = appNavController) @@ -238,7 +252,8 @@ fun MediaDetailScreen( windowSize = windowSize, showImageGallery = showGalleryOverlay, pagerState = pagerState, - mainViewModel = mainViewModel + mainViewModel = mainViewModel, + titleViewHidden = titleViewHidden ) PullRefreshIndicator( refreshing = isRefreshing.value, @@ -272,6 +287,7 @@ fun MediaViewContent( windowSize: WindowSizeClass, showImageGallery: MutableState, pagerState: PagerState, + titleViewHidden: MutableState, preferences: AppPreferences = KoinJavaComponent.get(AppPreferences::class.java) ) { Row( @@ -310,7 +326,8 @@ fun MediaViewContent( type = type, mediaItem = mediaItem, mainViewModel = mainViewModel, - itemId = itemId + itemId = itemId, + titleViewHidden = titleViewHidden ) ExternalIdsArea( @@ -404,6 +421,7 @@ fun MediaViewContent( private fun MiscTvDetails( itemId: Int, mediaItem: DetailedItem?, + titleViewHidden: MutableState, mainViewModel: MainViewModel ) { mediaItem?.let { tv -> @@ -421,7 +439,9 @@ private fun MiscTvDetails( runtime = TmdbUtils.convertRuntimeToHoursMinutes(series), genres = series.genres, contentRating = contentRating, - type = MediaViewType.TV + type = MediaViewType.TV, + title = mediaItem.title, + titleViewHidden = titleViewHidden ) } } @@ -430,6 +450,7 @@ private fun MiscTvDetails( private fun MiscMovieDetails( itemId: Int, mediaItem: DetailedItem?, + titleViewHidden: MutableState, mainViewModel: MainViewModel ) { val movie = mediaItem as? DetailedMovie @@ -446,18 +467,22 @@ private fun MiscMovieDetails( runtime = TmdbUtils.convertRuntimeToHoursMinutes(movie), genres = movie?.genres ?: emptyList(), contentRating = contentRating, - type = MediaViewType.MOVIE + type = MediaViewType.MOVIE, + title = mediaItem?.title ?: "", + titleViewHidden = titleViewHidden ) } @Composable private fun MiscDetails( modifier: Modifier = Modifier, + title: String, year: String, runtime: String, genres: List, contentRating: String, - type: MediaViewType + type: MediaViewType, + titleViewHidden: MutableState, ) { val mainViewModel = viewModel() val detailsLoadingState = remember { mainViewModel.produceDetailsLoadingStateFor(type) } @@ -473,6 +498,20 @@ private fun MiscDetails( modifier = modifier, verticalArrangement = Arrangement.spacedBy(8.dp) ) { + Text( + text = title.takeUnless { isLoading } ?: "", + color = MaterialTheme.colorScheme.secondary, + style = Typography.headlineLarge, + maxLines = 2, + overflow = TextOverflow.Ellipsis, + modifier = shimmerModifier + .width(400.dp) + .combinedOnVisibilityChange( + onVisible = { titleViewHidden.value = false }, + onNotVisible = { titleViewHidden.value = true } + ) + ) + Row( modifier = Modifier .fillMaxWidth() @@ -1434,19 +1473,22 @@ fun DetailsFor( type: MediaViewType, itemId: Int, mediaItem: DetailedItem?, + titleViewHidden: MutableState, mainViewModel: MainViewModel ) { if (type == MediaViewType.MOVIE) { MiscMovieDetails( itemId = itemId, mediaItem = mediaItem, - mainViewModel = mainViewModel + mainViewModel = mainViewModel, + titleViewHidden = titleViewHidden ) } else { MiscTvDetails( itemId = itemId, mediaItem = mediaItem, - mainViewModel = mainViewModel + mainViewModel = mainViewModel, + titleViewHidden = titleViewHidden ) } } diff --git a/app/src/main/java/com/owenlejeune/tvtime/ui/screens/PeopleDetailScreen.kt b/app/src/main/java/com/owenlejeune/tvtime/ui/screens/PeopleDetailScreen.kt index a95fe08..2d918e1 100644 --- a/app/src/main/java/com/owenlejeune/tvtime/ui/screens/PeopleDetailScreen.kt +++ b/app/src/main/java/com/owenlejeune/tvtime/ui/screens/PeopleDetailScreen.kt @@ -1,5 +1,8 @@ package com.owenlejeune.tvtime.ui.screens +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.Arrangement @@ -43,6 +46,7 @@ import androidx.navigation.NavController import com.google.accompanist.pager.ExperimentalPagerApi import com.owenlejeune.tvtime.R import com.owenlejeune.tvtime.api.tmdb.api.v3.model.DetailPerson +import com.owenlejeune.tvtime.extensions.combinedOnVisibilityChange import com.owenlejeune.tvtime.ui.components.BackButton import com.owenlejeune.tvtime.ui.components.ContentCard import com.owenlejeune.tvtime.ui.components.DetailHeader @@ -52,6 +56,7 @@ import com.owenlejeune.tvtime.ui.components.PosterItem import com.owenlejeune.tvtime.ui.components.TVTTopAppBar import com.owenlejeune.tvtime.ui.components.TwoLineImageTextCard import com.owenlejeune.tvtime.ui.navigation.AppNavItem +import com.owenlejeune.tvtime.ui.theme.Typography import com.owenlejeune.tvtime.ui.viewmodel.ApplicationViewModel import com.owenlejeune.tvtime.ui.viewmodel.MainViewModel import com.owenlejeune.tvtime.utils.TmdbUtils @@ -105,12 +110,21 @@ fun PersonDetailScreen( } ) + val titleViewHidden = remember { mutableStateOf(false) } Scaffold( modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), topBar = { TVTTopAppBar( scrollBehavior = scrollBehavior, - title = { Text(text = person?.name ?: "") }, + title = { + AnimatedVisibility( + visible = titleViewHidden.value, + enter = fadeIn(), + exit = fadeOut() + ) { + Text(text = person?.name ?: "") + } + }, appNavController = appNavController, navigationIcon = { BackButton(navController = appNavController) @@ -135,6 +149,20 @@ fun PersonDetailScreen( elevation = 0.dp ) + Text( + text = person?.name ?: "", + color = MaterialTheme.colorScheme.secondary, + style = Typography.headlineLarge, + maxLines = 2, + overflow = TextOverflow.Ellipsis, + modifier = Modifier + .fillMaxWidth() + .combinedOnVisibilityChange( + onVisible = { titleViewHidden.value = false }, + onNotVisible = { titleViewHidden.value = true } + ) + ) + ExternalIdsArea( modifier = Modifier.padding(start = 4.dp), type = MediaViewType.PERSON,