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

View File

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

View File

@@ -39,47 +39,47 @@ internal fun TvEpisodeHeroSection(
Column(
modifier = modifier
.fillMaxWidth()
.widthIn(max = 760.dp)
.widthIn(max = 560.dp)
) {
if (!seriesTitle.isNullOrBlank()) {
Text(
text = seriesTitle,
color = scheme.primary,
fontSize = 18.sp,
fontSize = 12.sp,
fontWeight = FontWeight.SemiBold,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
Spacer(modifier = Modifier.height(10.dp))
Spacer(modifier = Modifier.height(2.dp))
}
Text(
text = episode.title,
color = scheme.onBackground,
fontSize = 42.sp,
fontSize = 24.sp,
fontWeight = FontWeight.Bold,
lineHeight = 48.sp,
maxLines = 2,
lineHeight = 28.sp,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
Spacer(modifier = Modifier.height(8.dp))
Spacer(modifier = Modifier.height(2.dp))
Text(
text = "Episode ${episode.index}",
color = mutedStrong,
fontSize = 18.sp,
fontSize = 10.sp,
fontWeight = FontWeight.Medium
)
Spacer(modifier = Modifier.height(18.dp))
Spacer(modifier = Modifier.height(4.dp))
MediaMetadataFlowRow(
items = listOf(episode.releaseDate, episode.rating, episode.runtime, episode.format),
highlightedItem = episode.format
)
Spacer(modifier = Modifier.height(24.dp))
Spacer(modifier = Modifier.height(6.dp))
MediaResumeButton(
text = mediaPlayButtonText(episode.progress, episode.watched),
progress = mediaPlaybackProgress(episode.progress),
onClick = onPlay,
modifier = Modifier
.sizeIn(minWidth = 216.dp, maxWidth = 240.dp)
.sizeIn(minWidth = 160.dp, maxWidth = 192.dp)
.focusRequester(playFocusRequester)
.testTag(EpisodePlayButtonTag),
focusedScale = 1.08f,
@@ -88,7 +88,10 @@ internal fun TvEpisodeHeroSection(
focusBorderColor = scheme.onBackground,
overlayBorderWidth = 2.dp,
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(
backgroundImageUrl = tvMediaDetailBackgroundImageUrl(movie.imageUrlPrefix),
modifier = Modifier
modifier = it
) {
TvMovieHeroSection(
movie = movie,
@@ -75,31 +75,31 @@ internal fun TvMovieScreenContent(
playFocusRequester = playFocusRequester,
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(12.dp))
Spacer(modifier = Modifier.height(2.dp))
}
Column(modifier = Modifier.fillMaxWidth()) {
Spacer(modifier = Modifier.height(16.dp))
Column(modifier = it.fillMaxWidth()) {
Spacer(modifier = Modifier.height(4.dp))
MediaDetailOverviewSection(
synopsis = movie.synopsis,
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(20.dp))
Spacer(modifier = Modifier.height(8.dp))
}
}
Column(modifier = Modifier.fillMaxWidth()) {
Column(modifier = it.fillMaxWidth()) {
MediaDetailPlaybackSection(
audioTrack = "ENG",
subtitles = "ENG",
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(20.dp))
Spacer(modifier = Modifier.height(8.dp))
}
if (movie.cast.isNotEmpty()) {
Column(modifier = Modifier.fillMaxWidth()) {
Column(modifier = it.fillMaxWidth()) {
MediaDetailSectionTitle(text = "Cast")
Spacer(modifier = Modifier.height(14.dp))
// MediaCastRow(cast = movie.cast)
Spacer(modifier = Modifier.height(24.dp))
}
}
}
}

View File

@@ -37,29 +37,29 @@ internal fun TvMovieHeroSection(
Column(
modifier = modifier
.fillMaxWidth()
.widthIn(max = 760.dp)
.widthIn(max = 560.dp)
) {
Text(
text = movie.title,
color = scheme.onBackground,
fontSize = 42.sp,
fontSize = 24.sp,
fontWeight = FontWeight.Bold,
lineHeight = 48.sp,
maxLines = 2,
lineHeight = 28.sp,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
Spacer(modifier = Modifier.height(18.dp))
Spacer(modifier = Modifier.height(4.dp))
MediaMetadataFlowRow(
items = listOf(movie.year, movie.rating, movie.runtime, movie.format),
highlightedItem = movie.format
)
Spacer(modifier = Modifier.height(24.dp))
Spacer(modifier = Modifier.height(6.dp))
MediaResumeButton(
text = mediaPlayButtonText(movie.progress, movie.watched),
progress = mediaPlaybackProgress(movie.progress),
onClick = onPlay,
modifier = Modifier
.sizeIn(minWidth = 216.dp, maxWidth = 240.dp)
.sizeIn(minWidth = 160.dp, maxWidth = 192.dp)
.focusRequester(playFocusRequester)
.testTag(MoviePlayButtonTag),
focusedScale = 1.08f,
@@ -68,7 +68,10 @@ internal fun TvMovieHeroSection(
focusBorderColor = scheme.onBackground,
overlayBorderWidth = 2.dp,
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,
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(12.dp))
Spacer(modifier = Modifier.height(2.dp))
}
Column(modifier = it.fillMaxWidth()) {
Spacer(modifier = Modifier.height(16.dp))
Spacer(modifier = Modifier.height(4.dp))
MediaDetailOverviewSection(
synopsis = series.synopsis,
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(24.dp))
Spacer(modifier = Modifier.height(8.dp))
}
TvSeasonTabs(
seasons = series.seasons,
@@ -111,7 +111,7 @@ internal fun TvSeriesScreenContent(
onSelect = { selectedSeason = it },
modifier = it
)
Spacer(modifier = Modifier.height(20.dp))
Spacer(modifier = Modifier.height(8.dp))
TvEpisodeCarousel(
episodes = selectedSeason.episodes,
modifier = it
@@ -128,7 +128,6 @@ internal fun TvSeriesScreenContent(
}
}
private fun Series.defaultSeason(): Season {
for (season in seasons) {
if (season.episodes.any { !it.watched }) {

View File

@@ -191,54 +191,53 @@ internal fun TvSeriesHeroSection(
Column(
modifier = modifier
.fillMaxWidth()
.widthIn(max = 760.dp)
.widthIn(max = 560.dp)
) {
Text(
text = series.name,
color = scheme.onBackground,
fontSize = 42.sp,
fontSize = 24.sp,
fontWeight = FontWeight.Bold,
lineHeight = 48.sp,
maxLines = 2,
lineHeight = 28.sp,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
Spacer(modifier = Modifier.height(18.dp))
Spacer(modifier = Modifier.height(4.dp))
TvSeriesMetaChips(series = series)
Spacer(modifier = Modifier.height(24.dp))
Spacer(modifier = Modifier.height(6.dp))
if (nextUpEpisode != null) {
Text(
text = nextUpEpisode.heroStatusText(),
color = scheme.primary,
fontSize = 18.sp,
fontSize = 12.sp,
fontWeight = FontWeight.SemiBold,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
Spacer(modifier = Modifier.height(10.dp))
Spacer(modifier = Modifier.height(2.dp))
Text(
text = nextUpEpisode.title,
color = scheme.onBackground,
fontSize = 22.sp,
fontSize = 14.sp,
fontWeight = FontWeight.SemiBold,
maxLines = 2,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
Spacer(modifier = Modifier.height(6.dp))
Text(
text = "Episode ${nextUpEpisode.index}${nextUpEpisode.runtime}",
color = mutedStrong,
fontSize = 14.sp,
fontSize = 10.sp,
fontWeight = FontWeight.Medium,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
Spacer(modifier = Modifier.height(24.dp))
Spacer(modifier = Modifier.height(6.dp))
MediaResumeButton(
text = mediaPlayButtonText(nextUpEpisode.progress, nextUpEpisode.watched),
progress = mediaPlaybackProgress(nextUpEpisode.progress),
onClick = { onPlayEpisode(nextUpEpisode) },
modifier = Modifier
.sizeIn(minWidth = 216.dp, maxWidth = 240.dp)
.sizeIn(minWidth = 160.dp, maxWidth = 192.dp)
.focusRequester(playFocusRequester)
.focusProperties { down = firstContentFocusRequester }
.testTag(SeriesPlayButtonTag),
@@ -248,13 +247,16 @@ internal fun TvSeriesHeroSection(
focusBorderColor = scheme.onBackground,
overlayBorderWidth = 2.dp,
overlayBorderColor = scheme.primary.copy(alpha = 0.95f),
focusable = true
focusable = true,
height = 40.dp,
textSize = 13.sp,
iconSize = 20.dp
)
} else {
Text(
text = "Choose a season below to start watching.",
color = mutedStrong,
fontSize = 16.sp,
fontSize = 12.sp,
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.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape
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.text.font.FontWeight
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.TextUnit
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
@@ -50,7 +52,10 @@ fun MediaResumeButton(
focusBorderColor: Color = Color.Transparent,
overlayBorderWidth: Dp = 0.dp,
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 primaryColor = scheme.primary
@@ -73,7 +78,7 @@ fun MediaResumeButton(
BoxWithConstraints(
modifier = modifier
.graphicsLayer { scaleX = scale; scaleY = scale }
.height(52.dp)
.height(height)
.background(haloColor, shape)
.border(
width = if (isFocused) focusBorderWidth else 0.dp,
@@ -91,7 +96,12 @@ fun MediaResumeButton(
.background(onPrimaryColor),
contentAlignment = Alignment.Center
) {
ResumeButtonContent(text = text, color = primaryColor)
ResumeButtonContent(
text = text,
color = primaryColor,
textSize = textSize,
iconSize = iconSize
)
}
Box(
@@ -111,7 +121,12 @@ fun MediaResumeButton(
.background(primaryColor),
contentAlignment = Alignment.Center
) {
ResumeButtonContent(text = text, color = onPrimaryColor)
ResumeButtonContent(
text = text,
color = onPrimaryColor,
textSize = textSize,
iconSize = iconSize
)
}
if (isFocused && overlayBorderWidth > 0.dp) {
@@ -125,13 +140,23 @@ fun MediaResumeButton(
}
@Composable
private fun ResumeButtonContent(text: String, color: Color) {
private fun ResumeButtonContent(
text: String,
color: Color,
textSize: TextUnit,
iconSize: Dp
) {
Row(
verticalAlignment = Alignment.CenterVertically,
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))
Icon(Icons.Filled.PlayArrow, null, tint = color)
Icon(
imageVector = Icons.Filled.PlayArrow,
contentDescription = null,
tint = color,
modifier = Modifier.size(iconSize)
)
}
}