From 3047e53618ba7a73331f6339afc02f8f8efef50b Mon Sep 17 00:00:00 2001 From: Himanth Reddy Date: Sat, 30 May 2026 20:58:38 +0530 Subject: [PATCH 1/2] style: optimize home hero layout spacing, sizing, and alignment for TV UI --- .../tv/ui/screens/details/DetailsScreen.kt | 33 ++++--- .../arflix/tv/ui/screens/home/HomeScreen.kt | 98 +++++++++---------- 2 files changed, 64 insertions(+), 67 deletions(-) diff --git a/app/src/main/kotlin/com/arflix/tv/ui/screens/details/DetailsScreen.kt b/app/src/main/kotlin/com/arflix/tv/ui/screens/details/DetailsScreen.kt index e7b77b8c..c76612b7 100644 --- a/app/src/main/kotlin/com/arflix/tv/ui/screens/details/DetailsScreen.kt +++ b/app/src/main/kotlin/com/arflix/tv/ui/screens/details/DetailsScreen.kt @@ -40,6 +40,7 @@ import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.statusBars import androidx.compose.foundation.layout.statusBarsPadding import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.lazy.itemsIndexed as standardItemsIndexed import androidx.compose.foundation.rememberScrollState @@ -1838,7 +1839,7 @@ private fun DetailsContent( val ratingValue = parseRatingValue(rating) val primaryNetworkLogo = item.primaryNetworkLogo?.takeIf { it.isNotBlank() } val budgetText = budget?.trim()?.takeIf { it.isNotEmpty() && item.mediaType == MediaType.MOVIE } - val overviewMaxHeight = if (isCompactHeight) 68.dp else 72.dp + val overviewMaxHeight = if (isCompactHeight) 96.dp else 104.dp val separatorStyle = ArflixTypography.caption.copy( fontSize = 13.sp, @@ -1846,7 +1847,7 @@ private fun DetailsContent( ) Row( - horizontalArrangement = Arrangement.spacedBy(3.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp), verticalAlignment = Alignment.CenterVertically ) { Text( @@ -1860,7 +1861,7 @@ private fun DetailsContent( ) if (displayDate.isNotEmpty()) { - Text(text = "|", style = separatorStyle, color = Color.White.copy(alpha = 0.7f)) + Text(text = "•", style = separatorStyle, color = Color.White.copy(alpha = 0.5f)) Text( text = displayDate, style = ArflixTypography.caption.copy( @@ -1873,7 +1874,7 @@ private fun DetailsContent( } if (hasDuration) { - Text(text = "|", style = separatorStyle, color = Color.White.copy(alpha = 0.7f)) + Text(text = "•", style = separatorStyle, color = Color.White.copy(alpha = 0.5f)) Text( text = item.duration, style = ArflixTypography.caption.copy( @@ -1886,32 +1887,32 @@ private fun DetailsContent( } if (primaryNetworkLogo != null) { - Text(text = "|", style = separatorStyle, color = Color.White.copy(alpha = 0.7f)) + Box(modifier = Modifier.height(14.dp).width(1.dp).background(Color.White.copy(alpha = 0.3f))) AsyncImage( model = primaryNetworkLogo, imageLoader = metadataLogoImageLoader, contentDescription = "Primary streaming provider", contentScale = ContentScale.Fit, modifier = Modifier - .height(16.dp) - .width(52.dp) + .height(20.dp) + .widthIn(max = 64.dp) ) } if (ratingValue > 0f) { - Text(text = "|", style = separatorStyle, color = Color.White.copy(alpha = 0.7f)) + Box(modifier = Modifier.height(14.dp).width(1.dp).background(Color.White.copy(alpha = 0.3f))) DetailsImdbSvgRatingBadge( rating = rating, imageLoader = metadataLogoImageLoader, ratingFontSize = 13, - logoWidth = 34.dp, - logoHeight = 14.dp, + logoWidth = 48.dp, + logoHeight = 20.dp, textShadow = textShadow ) } if (!budgetText.isNullOrBlank()) { - Text(text = "|", style = separatorStyle, color = Color.White.copy(alpha = 0.7f)) + Box(modifier = Modifier.height(14.dp).width(1.dp).background(Color.White.copy(alpha = 0.3f))) Text( text = "${stringResource(R.string.budget)} $budgetText", style = ArflixTypography.caption.copy( @@ -1924,24 +1925,24 @@ private fun DetailsContent( } } - Spacer(modifier = Modifier.height(6.dp)) + Spacer(modifier = Modifier.height(24.dp)) val displayOverview = item.overview Box( modifier = Modifier - .width(360.dp) + .widthIn(max = 560.dp) .height(overviewMaxHeight) ) { Text( text = displayOverview, style = ArflixTypography.body.copy( - fontSize = 12.sp, + fontSize = 16.sp, fontWeight = FontWeight.Normal, - lineHeight = 16.sp, + lineHeight = 23.sp, shadow = textShadow ), - color = Color.White.copy(alpha = 0.9f), + color = Color(0xDFFFFFFF), maxLines = 4, overflow = TextOverflow.Ellipsis ) diff --git a/app/src/main/kotlin/com/arflix/tv/ui/screens/home/HomeScreen.kt b/app/src/main/kotlin/com/arflix/tv/ui/screens/home/HomeScreen.kt index 081edc98..976e66ed 100644 --- a/app/src/main/kotlin/com/arflix/tv/ui/screens/home/HomeScreen.kt +++ b/app/src/main/kotlin/com/arflix/tv/ui/screens/home/HomeScreen.kt @@ -47,6 +47,8 @@ import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.widthIn +import androidx.compose.foundation.layout.IntrinsicSize import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons @@ -1314,7 +1316,7 @@ private fun HeroSection( // Use primary shadow for text (Compose only supports one shadow per text) // But the frosted pill provides additional protection val textShadow = textShadowPrimary - val heroTextWidth = 360.dp + val heroTextWidth = 560.dp Column( modifier = modifier, @@ -1395,7 +1397,7 @@ private fun HeroSection( } } - Spacer(modifier = Modifier.height(4.dp)) + Spacer(modifier = Modifier.height(12.dp)) // Performance: Use key instead of AnimatedContent for faster transitions key(item.id) { @@ -1462,10 +1464,10 @@ private fun HeroSection( Column( modifier = Modifier.width(heroTextWidth), - verticalArrangement = Arrangement.spacedBy(3.dp) + verticalArrangement = Arrangement.spacedBy(8.dp) ) { Row( - horizontalArrangement = Arrangement.spacedBy(3.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp), verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth() ) { @@ -1473,7 +1475,7 @@ private fun HeroSection( Text( text = displayDate, style = ArflixTypography.caption.copy( - fontSize = 13.sp, + fontSize = 16.sp, fontWeight = FontWeight.Bold, shadow = textShadow ), @@ -1483,12 +1485,12 @@ private fun HeroSection( if (hasGenre || hasDuration) { Text( - text = "|", + text = "•", style = ArflixTypography.caption.copy( - fontSize = 13.sp, + fontSize = 16.sp, shadow = textShadow ), - color = Color.White.copy(alpha = 0.7f) + color = Color.White.copy(alpha = 0.5f) ) } } @@ -1497,7 +1499,7 @@ private fun HeroSection( Text( text = genreText, style = ArflixTypography.caption.copy( - fontSize = 13.sp, + fontSize = 16.sp, fontWeight = FontWeight.Bold, shadow = textShadow ), @@ -1511,18 +1513,18 @@ private fun HeroSection( if (hasDuration) { if (hasGenre) { Text( - text = "|", + text = "•", style = ArflixTypography.caption.copy( - fontSize = 13.sp, + fontSize = 16.sp, shadow = textShadow ), - color = Color.White.copy(alpha = 0.7f) + color = Color.White.copy(alpha = 0.5f) ) } Text( text = currentItem.duration, style = ArflixTypography.caption.copy( - fontSize = 13.sp, + fontSize = 16.sp, fontWeight = FontWeight.Bold, shadow = textShadow ), @@ -1534,9 +1536,8 @@ private fun HeroSection( if (hasSecondaryMetadata) { Row( - horizontalArrangement = Arrangement.spacedBy(6.dp), - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.fillMaxWidth() + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically ) { if (primaryNetworkLogo != null) { AsyncImage( @@ -1545,18 +1546,16 @@ private fun HeroSection( contentDescription = "Primary streaming provider", contentScale = ContentScale.Fit, modifier = Modifier - .height(16.dp) - .width(58.dp) + .height(24.dp) + .widthIn(max = 72.dp) ) if (hasRatingMetadata || hasBudgetMetadata) { - Text( - text = "|", - style = ArflixTypography.caption.copy( - fontSize = 12.sp, - shadow = textShadow - ), - color = Color.White.copy(alpha = 0.58f) + Box( + modifier = Modifier + .height(24.dp) + .width(1.dp) + .background(Color.White.copy(alpha = 0.3f)) ) } } @@ -1565,20 +1564,18 @@ private fun HeroSection( ImdbSvgRatingBadge( rating = rating, imageLoader = metadataLogoImageLoader, - ratingFontSize = 13, - logoWidth = 36.dp, - logoHeight = 15.dp, + ratingFontSize = 16, + logoWidth = 58.dp, + logoHeight = 24.dp, textShadow = textShadow ) if (hasBudgetMetadata) { - Text( - text = "|", - style = ArflixTypography.caption.copy( - fontSize = 12.sp, - shadow = textShadow - ), - color = Color.White.copy(alpha = 0.58f) + Box( + modifier = Modifier + .height(24.dp) + .width(1.dp) + .background(Color.White.copy(alpha = 0.3f)) ) } } @@ -1587,14 +1584,13 @@ private fun HeroSection( Text( text = "Budget $budgetText", style = ArflixTypography.caption.copy( - fontSize = 12.sp, + fontSize = 16.sp, fontWeight = FontWeight.Medium, shadow = textShadow ), color = Color.White.copy(alpha = 0.74f), maxLines = 1, - overflow = TextOverflow.Ellipsis, - modifier = Modifier.weight(1f, fill = false) + overflow = TextOverflow.Ellipsis ) } } @@ -1602,29 +1598,29 @@ private fun HeroSection( } } - Spacer(modifier = Modifier.height(6.dp)) + Spacer(modifier = Modifier.height(16.dp)) // Overview text (EPG data for IPTV, synopsis for movies/shows) val displayOverview = remember(overviewOverride, currentItem.overview) { cleanOverviewText(overviewOverride ?: currentItem.overview) } - val overviewMaxHeight = 72.dp + val overviewMaxHeight = 96.dp Box( modifier = Modifier - .width(360.dp) + .width(heroTextWidth) .height(overviewMaxHeight) ) { Text( text = displayOverview, style = ArflixTypography.body.copy( - fontSize = 12.sp, + fontSize = 16.sp, fontWeight = FontWeight.Normal, - lineHeight = 16.sp, + lineHeight = 22.4.sp, shadow = textShadow ), color = Color.White.copy(alpha = 0.9f), - maxLines = 4, + maxLines = 2, overflow = TextOverflow.Ellipsis ) } @@ -1709,11 +1705,8 @@ private fun HomeHeroLayer( } else { // TV hero: full-screen overlay with clearlogo val configuration = LocalConfiguration.current - val contentRowHeight = (configuration.screenHeightDp * 0.34f).dp.coerceIn(240.dp, 320.dp) - val contentRowBottomPadding = 12.dp - val contentRowTopPadding = contentRowHeight + contentRowBottomPadding - val buttonsBottomPadding = contentRowTopPadding - 10.dp - val heroBottomPadding = buttonsBottomPadding + if (configuration.screenHeightDp < 720) 34.dp else 34.dp + val rowsViewportHeight = (configuration.screenHeightDp * 0.31f).dp.coerceIn(260.dp, 340.dp) + val heroBottomMargin = rowsViewportHeight + 48.dp Box( modifier = Modifier @@ -1730,8 +1723,11 @@ private fun HomeHeroLayer( showBudget = showBudget, modifier = Modifier .align(Alignment.BottomStart) - .padding(start = contentStartPadding, end = 400.dp) - .offset(y = -heroBottomPadding) + .padding( + start = contentStartPadding, + end = 400.dp, + bottom = heroBottomMargin + ) ) } } From 1dfbfd6068c724beba44cd9f4135dcfaa1044692 Mon Sep 17 00:00:00 2001 From: Himanth Reddy Date: Sat, 30 May 2026 21:17:03 +0530 Subject: [PATCH 2/2] feat: implement graceful cinematic transitions for trailer playback --- .../arflix/tv/ui/components/TrailerPlayer.kt | 10 ++++-- .../arflix/tv/ui/screens/home/HomeScreen.kt | 34 +++++++++++++++++-- 2 files changed, 39 insertions(+), 5 deletions(-) diff --git a/app/src/main/kotlin/com/arflix/tv/ui/components/TrailerPlayer.kt b/app/src/main/kotlin/com/arflix/tv/ui/components/TrailerPlayer.kt index 458269b0..6814a430 100644 --- a/app/src/main/kotlin/com/arflix/tv/ui/components/TrailerPlayer.kt +++ b/app/src/main/kotlin/com/arflix/tv/ui/components/TrailerPlayer.kt @@ -50,7 +50,9 @@ fun TrailerPlayer( modifier: Modifier = Modifier, delayMs: Long = 0L, volume: Float = 0f, - onPlayingChanged: (Boolean) -> Unit = {} + onPlayingChanged: (Boolean) -> Unit = {}, + onFirstFrameRendered: () -> Unit = {}, + onEnded: () -> Unit = {} ) { val context = LocalContext.current var shouldPlay by remember { mutableStateOf(false) } @@ -82,7 +84,6 @@ fun TrailerPlayer( } if (videoUrl != null) { shouldPlay = true - onPlayingChanged(true) } else { onPlayingChanged(false) } @@ -106,8 +107,13 @@ fun TrailerPlayer( if (playbackState == Player.STATE_ENDED) { shouldPlay = false onPlayingChanged(false) + onEnded() } } + override fun onRenderedFirstFrame() { + onPlayingChanged(true) + onFirstFrameRendered() + } }) } } diff --git a/app/src/main/kotlin/com/arflix/tv/ui/screens/home/HomeScreen.kt b/app/src/main/kotlin/com/arflix/tv/ui/screens/home/HomeScreen.kt index 976e66ed..573b5b84 100644 --- a/app/src/main/kotlin/com/arflix/tv/ui/screens/home/HomeScreen.kt +++ b/app/src/main/kotlin/com/arflix/tv/ui/screens/home/HomeScreen.kt @@ -71,6 +71,7 @@ import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableLongStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberUpdatedState import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue @@ -182,6 +183,7 @@ import okhttp3.ConnectionPool import okhttp3.OkHttpClient import java.util.concurrent.TimeUnit import kotlinx.coroutines.delay +import kotlinx.coroutines.launch import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.distinctUntilChanged import kotlin.math.max @@ -843,9 +845,13 @@ fun HomeScreen( else -> null } + val scope = rememberCoroutineScope() var isTrailerPlaying by remember { mutableStateOf(false) } var trailerSuppressed by remember { mutableStateOf(false) } - LaunchedEffect(displayHeroItem?.id) { trailerSuppressed = false } + LaunchedEffect(displayHeroItem?.id) { + trailerSuppressed = false + isTrailerPlaying = false + } val heroRowIsContinueWatching = latestDisplayCategories .getOrNull(focusState.currentRowIndex)?.id == "continue_watching" val trailerOverlayAlpha = remember { Animatable(1f) } @@ -1020,7 +1026,19 @@ fun HomeScreen( youtubeKey = uiState.heroTrailerKey!!, delayMs = uiState.trailerDelaySeconds * 1000L, volume = if (uiState.trailerSoundEnabled) 1f else 0f, - onPlayingChanged = { playing -> isTrailerPlaying = playing }, + onFirstFrameRendered = { + scope.launch { + delay(500) + isTrailerPlaying = true + } + }, + onEnded = { + isTrailerPlaying = false + scope.launch { + delay(500) + trailerSuppressed = true + } + }, modifier = Modifier.fillMaxSize() ) } @@ -1099,7 +1117,17 @@ fun HomeScreen( usePosterCards = usePosterCards, isContextMenuOpen = showContextMenu, trailerIsPlaying = isTrailerPlaying, - onTrailerStop = { trailerSuppressed = true }, + onTrailerStop = { + if (isTrailerPlaying) { + isTrailerPlaying = false + scope.launch { + delay(500) + trailerSuppressed = true + } + } else { + trailerSuppressed = true + } + }, isMobile = isMobile, heroItem = displayHeroItem, heroOverviewOverride = displayHeroOverview,