Remove status and progress overlays from TV hero section

Removes eyebrow text, watched status chips, and progress bars from the
focused hero component on the TV home screen. The underlying model and
UI are simplified to display only the title and basic metadata.
This commit is contained in:
2026-04-06 19:38:27 +02:00
parent f0bb37d8ec
commit 0870e8aee4
2 changed files with 9 additions and 92 deletions

View File

@@ -16,12 +16,8 @@ import kotlin.math.roundToInt
internal data class TvFocusedHeroModel( internal data class TvFocusedHeroModel(
val id: UUID, val id: UUID,
val backdropImageUrl: String, val backdropImageUrl: String,
val eyebrowText: String,
val title: String, val title: String,
val metadata: List<String>, val metadata: List<String>,
val watchedText: String?,
val progressFraction: Float?,
val progressLabel: String?,
) { ) {
val metadataText: String? val metadataText: String?
get() = metadata.takeIf { it.isNotEmpty() }?.joinToString("") get() = metadata.takeIf { it.isNotEmpty() }?.joinToString("")
@@ -30,16 +26,16 @@ internal data class TvFocusedHeroModel(
internal fun FocusableItem.toTvFocusedHeroModel(): TvFocusedHeroModel { internal fun FocusableItem.toTvFocusedHeroModel(): TvFocusedHeroModel {
return when (this) { return when (this) {
is ContinueWatchingItem -> when (type) { is ContinueWatchingItem -> when (type) {
BaseItemKind.MOVIE -> movie!!.toTvFocusedHeroModel(sourceLabel = "Continue watching") BaseItemKind.MOVIE -> movie!!.toTvFocusedHeroModel()
BaseItemKind.EPISODE -> episode!!.toTvFocusedHeroModel(sourceLabel = "Continue watching") BaseItemKind.EPISODE -> episode!!.toTvFocusedHeroModel()
else -> unsupportedType(type) else -> unsupportedType(type)
} }
is NextUpItem -> episode.toTvFocusedHeroModel(sourceLabel = "Next up") is NextUpItem -> episode.toTvFocusedHeroModel()
is PosterItem -> when (type) { is PosterItem -> when (type) {
BaseItemKind.MOVIE -> movie!!.toTvFocusedHeroModel(sourceLabel = "Movie") BaseItemKind.MOVIE -> movie!!.toTvFocusedHeroModel()
BaseItemKind.EPISODE -> episode!!.toTvFocusedHeroModel(sourceLabel = "Episode") BaseItemKind.EPISODE -> episode!!.toTvFocusedHeroModel()
BaseItemKind.SERIES -> series!!.toTvFocusedHeroModel(sourceLabel = "Series") BaseItemKind.SERIES -> series!!.toTvFocusedHeroModel()
else -> unsupportedType(type) else -> unsupportedType(type)
} }
} }
@@ -50,32 +46,25 @@ internal fun backdropImageUrl(imageUrlPrefix: String?, fallbackImageUrl: String)
return backdropImageUrl.ifBlank { fallbackImageUrl } return backdropImageUrl.ifBlank { fallbackImageUrl }
} }
private fun Movie.toTvFocusedHeroModel(sourceLabel: String): TvFocusedHeroModel { private fun Movie.toTvFocusedHeroModel(): TvFocusedHeroModel {
val progressFraction = progressFraction(progress)
return TvFocusedHeroModel( return TvFocusedHeroModel(
id = id, id = id,
backdropImageUrl = backdropImageUrl( backdropImageUrl = backdropImageUrl(
imageUrlPrefix = imageUrlPrefix, imageUrlPrefix = imageUrlPrefix,
fallbackImageUrl = JellyfinImageHelper.finishImageUrl(imageUrlPrefix, ImageType.PRIMARY) fallbackImageUrl = JellyfinImageHelper.finishImageUrl(imageUrlPrefix, ImageType.PRIMARY)
), ),
eyebrowText = sourceLabel,
title = title, title = title,
metadata = listOf(year, rating, runtime, format).compactMetadata(), metadata = listOf(year, rating, runtime, format).compactMetadata(),
watchedText = "Watched".takeIf { watched },
progressFraction = progressFraction.takeIf { !watched && it != null },
progressLabel = progressLabel(progressFraction).takeIf { !watched && progressFraction != null },
) )
} }
private fun Episode.toTvFocusedHeroModel(sourceLabel: String): TvFocusedHeroModel { private fun Episode.toTvFocusedHeroModel(): TvFocusedHeroModel {
val progressFraction = progressFraction(progress)
return TvFocusedHeroModel( return TvFocusedHeroModel(
id = id, id = id,
backdropImageUrl = backdropImageUrl( backdropImageUrl = backdropImageUrl(
imageUrlPrefix = imageUrlPrefix, imageUrlPrefix = imageUrlPrefix,
fallbackImageUrl = JellyfinImageHelper.finishImageUrl(imageUrlPrefix, ImageType.PRIMARY) fallbackImageUrl = JellyfinImageHelper.finishImageUrl(imageUrlPrefix, ImageType.PRIMARY)
), ),
eyebrowText = sourceLabel,
title = title, title = title,
metadata = listOf( metadata = listOf(
"Episode $index", "Episode $index",
@@ -84,13 +73,10 @@ private fun Episode.toTvFocusedHeroModel(sourceLabel: String): TvFocusedHeroMode
rating, rating,
format format
).compactMetadata(), ).compactMetadata(),
watchedText = "Watched".takeIf { watched },
progressFraction = progressFraction.takeIf { !watched && it != null },
progressLabel = progressLabel(progressFraction).takeIf { !watched && progressFraction != null },
) )
} }
private fun Series.toTvFocusedHeroModel(sourceLabel: String): TvFocusedHeroModel { private fun Series.toTvFocusedHeroModel(): TvFocusedHeroModel {
val unwatchedText = if (unwatchedEpisodeCount > 0) { val unwatchedText = if (unwatchedEpisodeCount > 0) {
"$unwatchedEpisodeCount unwatched" "$unwatchedEpisodeCount unwatched"
} else { } else {
@@ -103,12 +89,8 @@ private fun Series.toTvFocusedHeroModel(sourceLabel: String): TvFocusedHeroModel
imageUrlPrefix = imageUrlPrefix, imageUrlPrefix = imageUrlPrefix,
fallbackImageUrl = JellyfinImageHelper.finishImageUrl(imageUrlPrefix, ImageType.PRIMARY) fallbackImageUrl = JellyfinImageHelper.finishImageUrl(imageUrlPrefix, ImageType.PRIMARY)
), ),
eyebrowText = sourceLabel,
title = name, title = name,
metadata = listOf(year, seasonLabel(seasonCount), unwatchedText).compactMetadata(), metadata = listOf(year, seasonLabel(seasonCount), unwatchedText).compactMetadata(),
watchedText = null,
progressFraction = null,
progressLabel = null,
) )
} }

View File

@@ -3,39 +3,29 @@ package hu.bbara.purefin.tv.home.ui
import androidx.compose.animation.Crossfade import androidx.compose.animation.Crossfade
import androidx.compose.animation.core.tween import androidx.compose.animation.core.tween
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.testTag import androidx.compose.ui.platform.testTag
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import hu.bbara.purefin.common.ui.components.MediaProgressBar
import hu.bbara.purefin.common.ui.components.PurefinAsyncImage import hu.bbara.purefin.common.ui.components.PurefinAsyncImage
internal const val TvHomeHeroTitleTag = "tv-home-hero-title" internal const val TvHomeHeroTitleTag = "tv-home-hero-title"
internal const val TvHomeHeroStatusTag = "tv-home-hero-status"
internal const val TvHomeHeroProgressLabelTag = "tv-home-hero-progress-label" internal const val TvHomeHeroProgressLabelTag = "tv-home-hero-progress-label"
private const val TvHomeHeroAnimationMillis = 180 private const val TvHomeHeroAnimationMillis = 180
@@ -107,13 +97,6 @@ internal fun TvFocusedItemHero(
verticalArrangement = Arrangement.spacedBy(8.dp), verticalArrangement = Arrangement.spacedBy(8.dp),
modifier = Modifier.widthIn(max = 720.dp) modifier = Modifier.widthIn(max = 720.dp)
) { ) {
Text(
text = hero.eyebrowText.uppercase(),
color = scheme.primary,
fontSize = 10.sp,
letterSpacing = 1.2.sp,
fontWeight = FontWeight.Bold
)
Text( Text(
text = hero.title, text = hero.title,
color = scheme.onBackground, color = scheme.onBackground,
@@ -134,54 +117,6 @@ internal fun TvFocusedItemHero(
overflow = TextOverflow.Ellipsis overflow = TextOverflow.Ellipsis
) )
} }
if (hero.watchedText != null || hero.progressFraction != null) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(10.dp),
modifier = Modifier
.clip(RoundedCornerShape(22.dp))
.background(scheme.surfaceContainerHigh.copy(alpha = 0.92f))
.border(1.dp, scheme.outlineVariant.copy(alpha = 0.6f), RoundedCornerShape(22.dp))
.padding(horizontal = 12.dp, vertical = 10.dp)
) {
hero.watchedText?.let { watchedText ->
TvHomeMetaChip(
text = watchedText,
highlighted = true,
modifier = Modifier.testTag(TvHomeHeroStatusTag)
)
}
if (hero.progressFraction != null && hero.progressLabel != null) {
Column(
verticalArrangement = Arrangement.spacedBy(4.dp),
modifier = Modifier.width(188.dp)
) {
Text(
text = "Progress",
color = scheme.onSurfaceVariant.copy(alpha = 0.85f),
fontSize = 10.sp,
fontWeight = FontWeight.Medium
)
MediaProgressBar(
progress = hero.progressFraction,
foregroundColor = scheme.primary,
backgroundColor = scheme.surfaceVariant,
contentPadding = PaddingValues(0.dp),
barHeight = 6.dp,
modifier = Modifier
)
}
Text(
text = hero.progressLabel,
color = scheme.onBackground,
fontSize = 14.sp,
fontWeight = FontWeight.SemiBold,
textAlign = TextAlign.End,
modifier = Modifier.testTag(TvHomeHeroProgressLabelTag)
)
}
}
}
} }
} }
} }