mirror of
https://github.com/bbara04/Purefin.git
synced 2026-07-23 19:26:50 +00:00
feat(tv): make hero responsive to screen height and enrich metadata
This commit is contained in:
@@ -10,7 +10,6 @@ import androidx.compose.runtime.Composable
|
|||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import hu.bbara.purefin.core.model.MediaUiModel
|
import hu.bbara.purefin.core.model.MediaUiModel
|
||||||
import hu.bbara.purefin.core.model.MovieUiModel
|
import hu.bbara.purefin.core.model.MovieUiModel
|
||||||
import hu.bbara.purefin.core.model.LibraryUiModel
|
import hu.bbara.purefin.core.model.LibraryUiModel
|
||||||
@@ -40,8 +39,7 @@ fun TvHomeScreen(
|
|||||||
modifier = Modifier.fillMaxSize()
|
modifier = Modifier.fillMaxSize()
|
||||||
) {
|
) {
|
||||||
TvFocusedItemHero(
|
TvFocusedItemHero(
|
||||||
item = focusedMediaUiModel.value,
|
item = focusedMediaUiModel.value
|
||||||
height = 220.dp
|
|
||||||
)
|
)
|
||||||
TvHomeContent(
|
TvHomeContent(
|
||||||
libraries = libraries,
|
libraries = libraries,
|
||||||
|
|||||||
@@ -6,9 +6,10 @@ import androidx.compose.foundation.background
|
|||||||
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.Row
|
||||||
|
import androidx.compose.foundation.layout.fillMaxHeight
|
||||||
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.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.widthIn
|
import androidx.compose.foundation.layout.widthIn
|
||||||
import androidx.compose.material3.MaterialTheme
|
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.platform.testTag
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
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.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import hu.bbara.purefin.core.model.MediaUiModel
|
import hu.bbara.purefin.core.model.MediaUiModel
|
||||||
import hu.bbara.purefin.ui.common.image.PurefinAsyncImage
|
import hu.bbara.purefin.ui.common.image.PurefinAsyncImage
|
||||||
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
internal const val TvHomeHeroTitleTag = "tv-home-hero-title"
|
internal const val TvHomeHeroTitleTag = "tv-home-hero-title"
|
||||||
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
|
||||||
|
// 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
|
@Composable
|
||||||
internal fun TvFocusedItemHero(
|
internal fun TvFocusedItemHero(
|
||||||
item: MediaUiModel,
|
item: MediaUiModel,
|
||||||
height: Dp,
|
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
|
heightFraction: Float = TvHomeHeroHeightFraction,
|
||||||
) {
|
) {
|
||||||
val scheme = MaterialTheme.colorScheme
|
val scheme = MaterialTheme.colorScheme
|
||||||
Box(
|
Box(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.height(height)
|
.fillMaxHeight(heightFraction)
|
||||||
.background(scheme.background)
|
.background(scheme.background)
|
||||||
) {
|
) {
|
||||||
Crossfade(
|
Crossfade(
|
||||||
@@ -56,20 +60,22 @@ internal fun TvFocusedItemHero(
|
|||||||
contentScale = ContentScale.Crop
|
contentScale = ContentScale.Crop
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
// Darken the left side so the text block stays readable over any backdrop.
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.background(
|
.background(
|
||||||
Brush.horizontalGradient(
|
Brush.horizontalGradient(
|
||||||
colorStops = arrayOf(
|
colorStops = arrayOf(
|
||||||
0.0f to scheme.background,
|
0.0f to scheme.background.copy(alpha = 0.86f),
|
||||||
0.28f to scheme.background.copy(alpha = 0.88f),
|
0.34f to scheme.background.copy(alpha = 0.62f),
|
||||||
0.62f to scheme.background.copy(alpha = 0.42f),
|
0.66f to scheme.background.copy(alpha = 0.22f),
|
||||||
1.0f to scheme.background.copy(alpha = 0.06f)
|
1.0f to scheme.background.copy(alpha = 0.04f)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
// Strong bottom fade so the title and metadata are legible.
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
@@ -77,7 +83,8 @@ internal fun TvFocusedItemHero(
|
|||||||
Brush.verticalGradient(
|
Brush.verticalGradient(
|
||||||
colorStops = arrayOf(
|
colorStops = arrayOf(
|
||||||
0.0f to scheme.background.copy(alpha = 0f),
|
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
|
1.0f to scheme.background
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -92,7 +99,7 @@ internal fun TvFocusedItemHero(
|
|||||||
verticalArrangement = Arrangement.Bottom,
|
verticalArrangement = Arrangement.Bottom,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.padding(horizontal = 40.dp, vertical = 18.dp)
|
.padding(horizontal = 40.dp, vertical = 16.dp)
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
@@ -101,19 +108,21 @@ internal fun TvFocusedItemHero(
|
|||||||
Text(
|
Text(
|
||||||
text = hero.primaryText,
|
text = hero.primaryText,
|
||||||
color = scheme.onBackground,
|
color = scheme.onBackground,
|
||||||
fontSize = 34.sp,
|
fontSize = 40.sp,
|
||||||
fontWeight = FontWeight.Bold,
|
fontWeight = FontWeight.Bold,
|
||||||
lineHeight = 38.sp,
|
lineHeight = 44.sp,
|
||||||
maxLines = 1,
|
maxLines = 2,
|
||||||
overflow = TextOverflow.Ellipsis,
|
overflow = TextOverflow.Ellipsis,
|
||||||
modifier = Modifier.testTag(TvHomeHeroTitleTag)
|
modifier = Modifier.testTag(TvHomeHeroTitleTag)
|
||||||
)
|
)
|
||||||
|
TvHomeHeroMetadataRow(item = hero)
|
||||||
|
if (hero.description.isNotBlank()) {
|
||||||
Text(
|
Text(
|
||||||
text = hero.description,
|
text = hero.description,
|
||||||
color = scheme.onSurfaceVariant,
|
color = scheme.onSurfaceVariant,
|
||||||
fontSize = 14.sp,
|
fontSize = 14.sp,
|
||||||
lineHeight = 18.sp,
|
lineHeight = 18.sp,
|
||||||
maxLines = 1,
|
maxLines = 2,
|
||||||
overflow = TextOverflow.Ellipsis
|
overflow = TextOverflow.Ellipsis
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -121,3 +130,28 @@ internal fun TvFocusedItemHero(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@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)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user