fix(tv): align detail screen sizing

This commit is contained in:
2026-04-29 23:37:23 +02:00
parent 522b93ff06
commit f444a5e09f
8 changed files with 111 additions and 81 deletions

View File

@@ -32,8 +32,8 @@ import hu.bbara.purefin.image.ImageUrlBuilder
import hu.bbara.purefin.ui.common.image.PurefinAsyncImage import hu.bbara.purefin.ui.common.image.PurefinAsyncImage
internal val MediaDetailHorizontalPadding = 48.dp internal val MediaDetailHorizontalPadding = 48.dp
private val MediaDetailHeaderTopPadding = 24.dp private val MediaDetailHeaderTopPadding = 6.dp
private val MediaDetailHeaderBottomPadding = 24.dp private val MediaDetailHeaderBottomPadding = 4.dp
private const val MediaDetailBodyImageWidthFraction = 0.66f private const val MediaDetailBodyImageWidthFraction = 0.66f
internal val TvMediaDetailBringIntoViewSpec: BringIntoViewSpec = internal val TvMediaDetailBringIntoViewSpec: BringIntoViewSpec =
@@ -77,7 +77,7 @@ internal fun TvMediaDetailScaffold(
internal fun TvMediaDetailBodyBox( internal fun TvMediaDetailBodyBox(
backgroundImageUrl: String, backgroundImageUrl: String,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
heightFraction: Float = 0.48f, heightFraction: Float = 0.24f,
content: @Composable ColumnScope.() -> Unit content: @Composable ColumnScope.() -> Unit
) { ) {
val scheme = MaterialTheme.colorScheme val scheme = MaterialTheme.colorScheme
@@ -173,12 +173,11 @@ internal fun MediaDetailOverviewSection(
title = "Overview", title = "Overview",
titleColor = scheme.onBackground, titleColor = scheme.onBackground,
bodyColor = scheme.onSurfaceVariant.copy(alpha = 0.85f), bodyColor = scheme.onSurfaceVariant.copy(alpha = 0.85f),
titleFontSize = 22.sp, titleFontSize = 15.sp,
bodyFontSize = 16.sp, bodyFontSize = 11.sp,
bodyLineHeight = 24.sp, bodyLineHeight = 14.sp,
titleSpacing = 14.dp, titleSpacing = 2.dp,
collapsedLines = 5, collapsedLines = 1
collapseInitially = false
) )
} }

View File

@@ -99,15 +99,15 @@ internal fun TvEpisodeScreenContent(
playFocusRequester = playFocusRequester, playFocusRequester = playFocusRequester,
modifier = Modifier.fillMaxWidth() modifier = Modifier.fillMaxWidth()
) )
Spacer(modifier = Modifier.height(12.dp)) Spacer(modifier = Modifier.height(2.dp))
} }
Column(modifier = it.fillMaxWidth()) { Column(modifier = it.fillMaxWidth()) {
Spacer(modifier = Modifier.height(16.dp)) Spacer(modifier = Modifier.height(4.dp))
MediaDetailOverviewSection( MediaDetailOverviewSection(
synopsis = episode.synopsis, synopsis = episode.synopsis,
modifier = Modifier.fillMaxWidth() modifier = Modifier.fillMaxWidth()
) )
Spacer(modifier = Modifier.height(20.dp)) Spacer(modifier = Modifier.height(8.dp))
} }
Column(modifier = it.fillMaxWidth()) { Column(modifier = it.fillMaxWidth()) {
MediaDetailPlaybackSection( MediaDetailPlaybackSection(
@@ -115,7 +115,7 @@ internal fun TvEpisodeScreenContent(
subtitles = "ENG", subtitles = "ENG",
modifier = Modifier.fillMaxWidth() modifier = Modifier.fillMaxWidth()
) )
Spacer(modifier = Modifier.height(20.dp)) Spacer(modifier = Modifier.height(8.dp))
} }
Column(modifier = it.fillMaxWidth()) { Column(modifier = it.fillMaxWidth()) {
MediaDetailSectionTitle(text = "Cast") MediaDetailSectionTitle(text = "Cast")
@@ -125,4 +125,3 @@ internal fun TvEpisodeScreenContent(
} }
} }
} }

View File

@@ -39,47 +39,47 @@ internal fun TvEpisodeHeroSection(
Column( Column(
modifier = modifier modifier = modifier
.fillMaxWidth() .fillMaxWidth()
.widthIn(max = 760.dp) .widthIn(max = 560.dp)
) { ) {
if (!seriesTitle.isNullOrBlank()) { if (!seriesTitle.isNullOrBlank()) {
Text( Text(
text = seriesTitle, text = seriesTitle,
color = scheme.primary, color = scheme.primary,
fontSize = 18.sp, fontSize = 12.sp,
fontWeight = FontWeight.SemiBold, fontWeight = FontWeight.SemiBold,
maxLines = 1, maxLines = 1,
overflow = TextOverflow.Ellipsis overflow = TextOverflow.Ellipsis
) )
Spacer(modifier = Modifier.height(10.dp)) Spacer(modifier = Modifier.height(2.dp))
} }
Text( Text(
text = episode.title, text = episode.title,
color = scheme.onBackground, color = scheme.onBackground,
fontSize = 42.sp, fontSize = 24.sp,
fontWeight = FontWeight.Bold, fontWeight = FontWeight.Bold,
lineHeight = 48.sp, lineHeight = 28.sp,
maxLines = 2, maxLines = 1,
overflow = TextOverflow.Ellipsis overflow = TextOverflow.Ellipsis
) )
Spacer(modifier = Modifier.height(8.dp)) Spacer(modifier = Modifier.height(2.dp))
Text( Text(
text = "Episode ${episode.index}", text = "Episode ${episode.index}",
color = mutedStrong, color = mutedStrong,
fontSize = 18.sp, fontSize = 10.sp,
fontWeight = FontWeight.Medium fontWeight = FontWeight.Medium
) )
Spacer(modifier = Modifier.height(18.dp)) Spacer(modifier = Modifier.height(4.dp))
MediaMetadataFlowRow( MediaMetadataFlowRow(
items = listOf(episode.releaseDate, episode.rating, episode.runtime, episode.format), items = listOf(episode.releaseDate, episode.rating, episode.runtime, episode.format),
highlightedItem = episode.format highlightedItem = episode.format
) )
Spacer(modifier = Modifier.height(24.dp)) Spacer(modifier = Modifier.height(6.dp))
MediaResumeButton( MediaResumeButton(
text = mediaPlayButtonText(episode.progress, episode.watched), text = mediaPlayButtonText(episode.progress, episode.watched),
progress = mediaPlaybackProgress(episode.progress), progress = mediaPlaybackProgress(episode.progress),
onClick = onPlay, onClick = onPlay,
modifier = Modifier modifier = Modifier
.sizeIn(minWidth = 216.dp, maxWidth = 240.dp) .sizeIn(minWidth = 160.dp, maxWidth = 192.dp)
.focusRequester(playFocusRequester) .focusRequester(playFocusRequester)
.testTag(EpisodePlayButtonTag), .testTag(EpisodePlayButtonTag),
focusedScale = 1.08f, focusedScale = 1.08f,
@@ -88,7 +88,10 @@ internal fun TvEpisodeHeroSection(
focusBorderColor = scheme.onBackground, focusBorderColor = scheme.onBackground,
overlayBorderWidth = 2.dp, overlayBorderWidth = 2.dp,
overlayBorderColor = scheme.primary.copy(alpha = 0.95f), overlayBorderColor = scheme.primary.copy(alpha = 0.95f),
focusable = true focusable = true,
height = 40.dp,
textSize = 13.sp,
iconSize = 20.dp
) )
} }
} }

View File

@@ -67,7 +67,7 @@ internal fun TvMovieScreenContent(
) { ) {
TvMediaDetailBodyBox( TvMediaDetailBodyBox(
backgroundImageUrl = tvMediaDetailBackgroundImageUrl(movie.imageUrlPrefix), backgroundImageUrl = tvMediaDetailBackgroundImageUrl(movie.imageUrlPrefix),
modifier = Modifier modifier = it
) { ) {
TvMovieHeroSection( TvMovieHeroSection(
movie = movie, movie = movie,
@@ -75,31 +75,31 @@ internal fun TvMovieScreenContent(
playFocusRequester = playFocusRequester, playFocusRequester = playFocusRequester,
modifier = Modifier.fillMaxWidth() modifier = Modifier.fillMaxWidth()
) )
Spacer(modifier = Modifier.height(12.dp)) Spacer(modifier = Modifier.height(2.dp))
} }
Column(modifier = Modifier.fillMaxWidth()) { Column(modifier = it.fillMaxWidth()) {
Spacer(modifier = Modifier.height(16.dp)) Spacer(modifier = Modifier.height(4.dp))
MediaDetailOverviewSection( MediaDetailOverviewSection(
synopsis = movie.synopsis, synopsis = movie.synopsis,
modifier = Modifier.fillMaxWidth() modifier = Modifier.fillMaxWidth()
) )
Spacer(modifier = Modifier.height(20.dp)) Spacer(modifier = Modifier.height(8.dp))
} }
} Column(modifier = it.fillMaxWidth()) {
Column(modifier = Modifier.fillMaxWidth()) {
MediaDetailPlaybackSection( MediaDetailPlaybackSection(
audioTrack = "ENG", audioTrack = "ENG",
subtitles = "ENG", subtitles = "ENG",
modifier = Modifier.fillMaxWidth() modifier = Modifier.fillMaxWidth()
) )
Spacer(modifier = Modifier.height(20.dp)) Spacer(modifier = Modifier.height(8.dp))
} }
if (movie.cast.isNotEmpty()) { if (movie.cast.isNotEmpty()) {
Column(modifier = Modifier.fillMaxWidth()) { Column(modifier = it.fillMaxWidth()) {
MediaDetailSectionTitle(text = "Cast") MediaDetailSectionTitle(text = "Cast")
Spacer(modifier = Modifier.height(14.dp)) Spacer(modifier = Modifier.height(14.dp))
// MediaCastRow(cast = movie.cast) // MediaCastRow(cast = movie.cast)
Spacer(modifier = Modifier.height(24.dp)) Spacer(modifier = Modifier.height(24.dp))
} }
} }
}
} }

View File

@@ -37,29 +37,29 @@ internal fun TvMovieHeroSection(
Column( Column(
modifier = modifier modifier = modifier
.fillMaxWidth() .fillMaxWidth()
.widthIn(max = 760.dp) .widthIn(max = 560.dp)
) { ) {
Text( Text(
text = movie.title, text = movie.title,
color = scheme.onBackground, color = scheme.onBackground,
fontSize = 42.sp, fontSize = 24.sp,
fontWeight = FontWeight.Bold, fontWeight = FontWeight.Bold,
lineHeight = 48.sp, lineHeight = 28.sp,
maxLines = 2, maxLines = 1,
overflow = TextOverflow.Ellipsis overflow = TextOverflow.Ellipsis
) )
Spacer(modifier = Modifier.height(18.dp)) Spacer(modifier = Modifier.height(4.dp))
MediaMetadataFlowRow( MediaMetadataFlowRow(
items = listOf(movie.year, movie.rating, movie.runtime, movie.format), items = listOf(movie.year, movie.rating, movie.runtime, movie.format),
highlightedItem = movie.format highlightedItem = movie.format
) )
Spacer(modifier = Modifier.height(24.dp)) Spacer(modifier = Modifier.height(6.dp))
MediaResumeButton( MediaResumeButton(
text = mediaPlayButtonText(movie.progress, movie.watched), text = mediaPlayButtonText(movie.progress, movie.watched),
progress = mediaPlaybackProgress(movie.progress), progress = mediaPlaybackProgress(movie.progress),
onClick = onPlay, onClick = onPlay,
modifier = Modifier modifier = Modifier
.sizeIn(minWidth = 216.dp, maxWidth = 240.dp) .sizeIn(minWidth = 160.dp, maxWidth = 192.dp)
.focusRequester(playFocusRequester) .focusRequester(playFocusRequester)
.testTag(MoviePlayButtonTag), .testTag(MoviePlayButtonTag),
focusedScale = 1.08f, focusedScale = 1.08f,
@@ -68,7 +68,10 @@ internal fun TvMovieHeroSection(
focusBorderColor = scheme.onBackground, focusBorderColor = scheme.onBackground,
overlayBorderWidth = 2.dp, overlayBorderWidth = 2.dp,
overlayBorderColor = scheme.primary.copy(alpha = 0.95f), overlayBorderColor = scheme.primary.copy(alpha = 0.95f),
focusable = true focusable = true,
height = 40.dp,
textSize = 13.sp,
iconSize = 20.dp
) )
} }
} }

View File

@@ -93,15 +93,15 @@ internal fun TvSeriesScreenContent(
firstContentFocusRequester = firstContentFocusRequester, firstContentFocusRequester = firstContentFocusRequester,
modifier = Modifier.fillMaxWidth() modifier = Modifier.fillMaxWidth()
) )
Spacer(modifier = Modifier.height(12.dp)) Spacer(modifier = Modifier.height(2.dp))
} }
Column(modifier = it.fillMaxWidth()) { Column(modifier = it.fillMaxWidth()) {
Spacer(modifier = Modifier.height(16.dp)) Spacer(modifier = Modifier.height(4.dp))
MediaDetailOverviewSection( MediaDetailOverviewSection(
synopsis = series.synopsis, synopsis = series.synopsis,
modifier = Modifier.fillMaxWidth() modifier = Modifier.fillMaxWidth()
) )
Spacer(modifier = Modifier.height(24.dp)) Spacer(modifier = Modifier.height(8.dp))
} }
TvSeasonTabs( TvSeasonTabs(
seasons = series.seasons, seasons = series.seasons,
@@ -111,7 +111,7 @@ internal fun TvSeriesScreenContent(
onSelect = { selectedSeason = it }, onSelect = { selectedSeason = it },
modifier = it modifier = it
) )
Spacer(modifier = Modifier.height(20.dp)) Spacer(modifier = Modifier.height(8.dp))
TvEpisodeCarousel( TvEpisodeCarousel(
episodes = selectedSeason.episodes, episodes = selectedSeason.episodes,
modifier = it modifier = it
@@ -128,7 +128,6 @@ internal fun TvSeriesScreenContent(
} }
} }
private fun Series.defaultSeason(): Season { private fun Series.defaultSeason(): Season {
for (season in seasons) { for (season in seasons) {
if (season.episodes.any { !it.watched }) { if (season.episodes.any { !it.watched }) {

View File

@@ -191,54 +191,53 @@ internal fun TvSeriesHeroSection(
Column( Column(
modifier = modifier modifier = modifier
.fillMaxWidth() .fillMaxWidth()
.widthIn(max = 760.dp) .widthIn(max = 560.dp)
) { ) {
Text( Text(
text = series.name, text = series.name,
color = scheme.onBackground, color = scheme.onBackground,
fontSize = 42.sp, fontSize = 24.sp,
fontWeight = FontWeight.Bold, fontWeight = FontWeight.Bold,
lineHeight = 48.sp, lineHeight = 28.sp,
maxLines = 2, maxLines = 1,
overflow = TextOverflow.Ellipsis overflow = TextOverflow.Ellipsis
) )
Spacer(modifier = Modifier.height(18.dp)) Spacer(modifier = Modifier.height(4.dp))
TvSeriesMetaChips(series = series) TvSeriesMetaChips(series = series)
Spacer(modifier = Modifier.height(24.dp)) Spacer(modifier = Modifier.height(6.dp))
if (nextUpEpisode != null) { if (nextUpEpisode != null) {
Text( Text(
text = nextUpEpisode.heroStatusText(), text = nextUpEpisode.heroStatusText(),
color = scheme.primary, color = scheme.primary,
fontSize = 18.sp, fontSize = 12.sp,
fontWeight = FontWeight.SemiBold, fontWeight = FontWeight.SemiBold,
maxLines = 1, maxLines = 1,
overflow = TextOverflow.Ellipsis overflow = TextOverflow.Ellipsis
) )
Spacer(modifier = Modifier.height(10.dp)) Spacer(modifier = Modifier.height(2.dp))
Text( Text(
text = nextUpEpisode.title, text = nextUpEpisode.title,
color = scheme.onBackground, color = scheme.onBackground,
fontSize = 22.sp, fontSize = 14.sp,
fontWeight = FontWeight.SemiBold, fontWeight = FontWeight.SemiBold,
maxLines = 2, maxLines = 1,
overflow = TextOverflow.Ellipsis overflow = TextOverflow.Ellipsis
) )
Spacer(modifier = Modifier.height(6.dp))
Text( Text(
text = "Episode ${nextUpEpisode.index}${nextUpEpisode.runtime}", text = "Episode ${nextUpEpisode.index}${nextUpEpisode.runtime}",
color = mutedStrong, color = mutedStrong,
fontSize = 14.sp, fontSize = 10.sp,
fontWeight = FontWeight.Medium, fontWeight = FontWeight.Medium,
maxLines = 1, maxLines = 1,
overflow = TextOverflow.Ellipsis overflow = TextOverflow.Ellipsis
) )
Spacer(modifier = Modifier.height(24.dp)) Spacer(modifier = Modifier.height(6.dp))
MediaResumeButton( MediaResumeButton(
text = mediaPlayButtonText(nextUpEpisode.progress, nextUpEpisode.watched), text = mediaPlayButtonText(nextUpEpisode.progress, nextUpEpisode.watched),
progress = mediaPlaybackProgress(nextUpEpisode.progress), progress = mediaPlaybackProgress(nextUpEpisode.progress),
onClick = { onPlayEpisode(nextUpEpisode) }, onClick = { onPlayEpisode(nextUpEpisode) },
modifier = Modifier modifier = Modifier
.sizeIn(minWidth = 216.dp, maxWidth = 240.dp) .sizeIn(minWidth = 160.dp, maxWidth = 192.dp)
.focusRequester(playFocusRequester) .focusRequester(playFocusRequester)
.focusProperties { down = firstContentFocusRequester } .focusProperties { down = firstContentFocusRequester }
.testTag(SeriesPlayButtonTag), .testTag(SeriesPlayButtonTag),
@@ -248,13 +247,16 @@ internal fun TvSeriesHeroSection(
focusBorderColor = scheme.onBackground, focusBorderColor = scheme.onBackground,
overlayBorderWidth = 2.dp, overlayBorderWidth = 2.dp,
overlayBorderColor = scheme.primary.copy(alpha = 0.95f), overlayBorderColor = scheme.primary.copy(alpha = 0.95f),
focusable = true focusable = true,
height = 40.dp,
textSize = 13.sp,
iconSize = 20.dp
) )
} else { } else {
Text( Text(
text = "Choose a season below to start watching.", text = "Choose a season below to start watching.",
color = mutedStrong, color = mutedStrong,
fontSize = 16.sp, fontSize = 12.sp,
fontWeight = FontWeight.Medium fontWeight = FontWeight.Medium
) )
} }

View File

@@ -13,6 +13,7 @@ import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
@@ -35,6 +36,7 @@ import androidx.compose.ui.graphics.drawscope.clipRect
import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.TextUnit
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
@@ -50,7 +52,10 @@ fun MediaResumeButton(
focusBorderColor: Color = Color.Transparent, focusBorderColor: Color = Color.Transparent,
overlayBorderWidth: Dp = 0.dp, overlayBorderWidth: Dp = 0.dp,
overlayBorderColor: Color = Color.Transparent, overlayBorderColor: Color = Color.Transparent,
focusable: Boolean = false focusable: Boolean = false,
height: Dp = 52.dp,
textSize: TextUnit = 16.sp,
iconSize: Dp = 24.dp
) { ) {
val scheme = MaterialTheme.colorScheme val scheme = MaterialTheme.colorScheme
val primaryColor = scheme.primary val primaryColor = scheme.primary
@@ -73,7 +78,7 @@ fun MediaResumeButton(
BoxWithConstraints( BoxWithConstraints(
modifier = modifier modifier = modifier
.graphicsLayer { scaleX = scale; scaleY = scale } .graphicsLayer { scaleX = scale; scaleY = scale }
.height(52.dp) .height(height)
.background(haloColor, shape) .background(haloColor, shape)
.border( .border(
width = if (isFocused) focusBorderWidth else 0.dp, width = if (isFocused) focusBorderWidth else 0.dp,
@@ -91,7 +96,12 @@ fun MediaResumeButton(
.background(onPrimaryColor), .background(onPrimaryColor),
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ) {
ResumeButtonContent(text = text, color = primaryColor) ResumeButtonContent(
text = text,
color = primaryColor,
textSize = textSize,
iconSize = iconSize
)
} }
Box( Box(
@@ -111,7 +121,12 @@ fun MediaResumeButton(
.background(primaryColor), .background(primaryColor),
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ) {
ResumeButtonContent(text = text, color = onPrimaryColor) ResumeButtonContent(
text = text,
color = onPrimaryColor,
textSize = textSize,
iconSize = iconSize
)
} }
if (isFocused && overlayBorderWidth > 0.dp) { if (isFocused && overlayBorderWidth > 0.dp) {
@@ -125,13 +140,23 @@ fun MediaResumeButton(
} }
@Composable @Composable
private fun ResumeButtonContent(text: String, color: Color) { private fun ResumeButtonContent(
text: String,
color: Color,
textSize: TextUnit,
iconSize: Dp
) {
Row( Row(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center horizontalArrangement = Arrangement.Center
) { ) {
Text(text, color = color, fontWeight = FontWeight.Bold, fontSize = 16.sp) Text(text, color = color, fontWeight = FontWeight.Bold, fontSize = textSize)
Spacer(Modifier.width(8.dp)) Spacer(Modifier.width(8.dp))
Icon(Icons.Filled.PlayArrow, null, tint = color) Icon(
imageVector = Icons.Filled.PlayArrow,
contentDescription = null,
tint = color,
modifier = Modifier.size(iconSize)
)
} }
} }