feat(tv): make hero responsive to screen height and enrich metadata

This commit is contained in:
2026-06-19 17:04:55 +00:00
parent eb540a0fbb
commit 831abfebb4
2 changed files with 56 additions and 24 deletions

View File

@@ -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,

View File

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