From 831abfebb4cdec0e205821ce1e153e54bd4d4480 Mon Sep 17 00:00:00 2001 From: Barnabas Balogh Date: Fri, 19 Jun 2026 17:04:55 +0000 Subject: [PATCH] feat(tv): make hero responsive to screen height and enrich metadata --- .../purefin/ui/screen/home/TvHomeScreen.kt | 4 +- .../home/components/TvFocusedItemHero.kt | 76 ++++++++++++++----- 2 files changed, 56 insertions(+), 24 deletions(-) diff --git a/app-tv/src/main/java/hu/bbara/purefin/ui/screen/home/TvHomeScreen.kt b/app-tv/src/main/java/hu/bbara/purefin/ui/screen/home/TvHomeScreen.kt index 947c08b2..2e0f8ffc 100644 --- a/app-tv/src/main/java/hu/bbara/purefin/ui/screen/home/TvHomeScreen.kt +++ b/app-tv/src/main/java/hu/bbara/purefin/ui/screen/home/TvHomeScreen.kt @@ -10,7 +10,6 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp import hu.bbara.purefin.core.model.MediaUiModel import hu.bbara.purefin.core.model.MovieUiModel import hu.bbara.purefin.core.model.LibraryUiModel @@ -40,8 +39,7 @@ fun TvHomeScreen( modifier = Modifier.fillMaxSize() ) { TvFocusedItemHero( - item = focusedMediaUiModel.value, - height = 220.dp + item = focusedMediaUiModel.value ) TvHomeContent( libraries = libraries, diff --git a/app-tv/src/main/java/hu/bbara/purefin/ui/screen/home/components/TvFocusedItemHero.kt b/app-tv/src/main/java/hu/bbara/purefin/ui/screen/home/components/TvFocusedItemHero.kt index d4001fbb..6023fe3f 100644 --- a/app-tv/src/main/java/hu/bbara/purefin/ui/screen/home/components/TvFocusedItemHero.kt +++ b/app-tv/src/main/java/hu/bbara/purefin/ui/screen/home/components/TvFocusedItemHero.kt @@ -6,9 +6,10 @@ import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.widthIn import androidx.compose.material3.MaterialTheme @@ -20,28 +21,31 @@ import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.testTag import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import hu.bbara.purefin.core.model.MediaUiModel import hu.bbara.purefin.ui.common.image.PurefinAsyncImage +import kotlin.math.roundToInt internal const val TvHomeHeroTitleTag = "tv-home-hero-title" internal const val TvHomeHeroProgressLabelTag = "tv-home-hero-progress-label" private const val TvHomeHeroAnimationMillis = 180 +// Half the screen for the billboard, half for the content rows. Tuned so both +// the hero block and the first focused row fit on a 540dp-tall (1080p xhdpi) TV. +internal const val TvHomeHeroHeightFraction = 0.5f @Composable internal fun TvFocusedItemHero( item: MediaUiModel, - height: Dp, modifier: Modifier = Modifier, + heightFraction: Float = TvHomeHeroHeightFraction, ) { val scheme = MaterialTheme.colorScheme Box( modifier = modifier .fillMaxWidth() - .height(height) + .fillMaxHeight(heightFraction) .background(scheme.background) ) { Crossfade( @@ -56,20 +60,22 @@ internal fun TvFocusedItemHero( contentScale = ContentScale.Crop ) } + // Darken the left side so the text block stays readable over any backdrop. Box( modifier = Modifier .fillMaxSize() .background( Brush.horizontalGradient( colorStops = arrayOf( - 0.0f to scheme.background, - 0.28f to scheme.background.copy(alpha = 0.88f), - 0.62f to scheme.background.copy(alpha = 0.42f), - 1.0f to scheme.background.copy(alpha = 0.06f) + 0.0f to scheme.background.copy(alpha = 0.86f), + 0.34f to scheme.background.copy(alpha = 0.62f), + 0.66f to scheme.background.copy(alpha = 0.22f), + 1.0f to scheme.background.copy(alpha = 0.04f) ) ) ) ) + // Strong bottom fade so the title and metadata are legible. Box( modifier = Modifier .fillMaxSize() @@ -77,7 +83,8 @@ internal fun TvFocusedItemHero( Brush.verticalGradient( colorStops = arrayOf( 0.0f to scheme.background.copy(alpha = 0f), - 0.56f to scheme.background.copy(alpha = 0.1f), + 0.40f to scheme.background.copy(alpha = 0.10f), + 0.72f to scheme.background.copy(alpha = 0.58f), 1.0f to scheme.background ) ) @@ -92,7 +99,7 @@ internal fun TvFocusedItemHero( verticalArrangement = Arrangement.Bottom, modifier = Modifier .fillMaxSize() - .padding(horizontal = 40.dp, vertical = 18.dp) + .padding(horizontal = 40.dp, vertical = 16.dp) ) { Column( verticalArrangement = Arrangement.spacedBy(8.dp), @@ -101,23 +108,50 @@ internal fun TvFocusedItemHero( Text( text = hero.primaryText, color = scheme.onBackground, - fontSize = 34.sp, + fontSize = 40.sp, fontWeight = FontWeight.Bold, - lineHeight = 38.sp, - maxLines = 1, + lineHeight = 44.sp, + maxLines = 2, overflow = TextOverflow.Ellipsis, modifier = Modifier.testTag(TvHomeHeroTitleTag) ) - Text( - text = hero.description, - color = scheme.onSurfaceVariant, - fontSize = 14.sp, - lineHeight = 18.sp, - maxLines = 1, - overflow = TextOverflow.Ellipsis - ) + TvHomeHeroMetadataRow(item = hero) + if (hero.description.isNotBlank()) { + Text( + text = hero.description, + color = scheme.onSurfaceVariant, + fontSize = 14.sp, + lineHeight = 18.sp, + maxLines = 2, + overflow = TextOverflow.Ellipsis + ) + } } } } } } + +@Composable +private fun TvHomeHeroMetadataRow(item: MediaUiModel) { + val hasSecondary = item.secondaryText.isNotBlank() + val progress = item.progress + val hasProgress = progress != null && progress > 0f && progress < 1f + if (!hasSecondary && !hasProgress) return + Row( + horizontalArrangement = Arrangement.spacedBy(8.dp), + modifier = Modifier.fillMaxWidth() + ) { + if (hasSecondary) { + TvHomeMetaChip(text = item.secondaryText) + } + if (hasProgress) { + val percent = ((progress ?: 0f) * 100f).roundToInt() + TvHomeMetaChip( + text = "Resume \u00B7 $percent%", + highlighted = true, + modifier = Modifier.testTag(TvHomeHeroProgressLabelTag) + ) + } + } +}