0.2.89
This commit is contained in:
@@ -64,7 +64,7 @@ val projectNoticeText =
|
|||||||
|
|
||||||
// A release workflow can derive the app version from its Git tag without editing the
|
// A release workflow can derive the app version from its Git tag without editing the
|
||||||
// source tree. Local builds keep using the checked-in default.
|
// source tree. Local builds keep using the checked-in default.
|
||||||
val defaultVersionName = "0.2.88"
|
val defaultVersionName = "0.2.89"
|
||||||
val membyVersionName: String =
|
val membyVersionName: String =
|
||||||
(project.findProperty("memby.versionName") as String?)
|
(project.findProperty("memby.versionName") as String?)
|
||||||
?.trim()
|
?.trim()
|
||||||
|
|||||||
@@ -140,6 +140,7 @@ private val QuietText: Color get() = MembyQuietText
|
|||||||
internal val TvRailCollapsedWidth = 54.dp
|
internal val TvRailCollapsedWidth = 54.dp
|
||||||
internal val TvRailExpandedWidth = 184.dp
|
internal val TvRailExpandedWidth = 184.dp
|
||||||
internal val TvRailContentShift = 112.dp
|
internal val TvRailContentShift = 112.dp
|
||||||
|
internal val HomeContentHorizontalInset = 48.dp
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The rail's order, which is this declaration order — see [navigationRailItems], the only
|
* The rail's order, which is this declaration order — see [navigationRailItems], the only
|
||||||
@@ -1136,14 +1137,19 @@ internal fun MetadataHero(
|
|||||||
sectionLabel = sectionLabel,
|
sectionLabel = sectionLabel,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.padding(start = 36.dp, end = 36.dp, top = 24.dp, bottom = 10.dp),
|
.padding(
|
||||||
|
start = HomeContentHorizontalInset,
|
||||||
|
end = HomeContentHorizontalInset,
|
||||||
|
top = 28.dp,
|
||||||
|
bottom = 18.dp,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal fun metadataPanelContentWidth(availableWidth: Dp, compact: Boolean): Dp =
|
internal fun metadataPanelContentWidth(availableWidth: Dp, compact: Boolean): Dp =
|
||||||
(availableWidth * if (compact) 0.84f else 0.76f)
|
(availableWidth * if (compact) 0.84f else 0.76f)
|
||||||
.coerceIn(280.dp, 720.dp)
|
.coerceIn(280.dp, 680.dp)
|
||||||
.coerceAtMost(availableWidth)
|
.coerceAtMost(availableWidth)
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -1436,7 +1442,7 @@ private fun MetadataContent(
|
|||||||
}
|
}
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier.width(contentWidth).fillMaxHeight(),
|
modifier = Modifier.width(contentWidth).fillMaxHeight(),
|
||||||
verticalArrangement = Arrangement.spacedBy(if (compact) 4.dp else 6.dp),
|
verticalArrangement = Arrangement.spacedBy(if (compact) 5.dp else 8.dp),
|
||||||
) {
|
) {
|
||||||
Text(sectionLabel.uppercase(), color = EmbyGreen, fontSize = 11.sp, fontWeight = FontWeight.Bold, letterSpacing = 1.1.sp)
|
Text(sectionLabel.uppercase(), color = EmbyGreen, fontSize = 11.sp, fontWeight = FontWeight.Bold, letterSpacing = 1.1.sp)
|
||||||
Text(
|
Text(
|
||||||
@@ -1473,7 +1479,7 @@ private fun MetadataContent(
|
|||||||
ItemRatingsStrip(
|
ItemRatingsStrip(
|
||||||
item = item,
|
item = item,
|
||||||
load = true,
|
load = true,
|
||||||
compact = compact,
|
compact = true,
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
)
|
)
|
||||||
val badges = buildList {
|
val badges = buildList {
|
||||||
@@ -1481,13 +1487,8 @@ private fun MetadataContent(
|
|||||||
addAll(mediaBadges(item))
|
addAll(mediaBadges(item))
|
||||||
}
|
}
|
||||||
item.membyRecommendationReason?.takeIf(String::isNotBlank)?.let {
|
item.membyRecommendationReason?.takeIf(String::isNotBlank)?.let {
|
||||||
Text(
|
RecommendationReasonCaption(
|
||||||
text = it,
|
text = recommendationReasonSummary(it),
|
||||||
color = EmbyGreen,
|
|
||||||
fontSize = 13.sp,
|
|
||||||
fontWeight = FontWeight.SemiBold,
|
|
||||||
maxLines = 2,
|
|
||||||
overflow = TextOverflow.Ellipsis,
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -1518,7 +1519,7 @@ private fun ScheduleMetadataContent(
|
|||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier.width(contentWidth).fillMaxHeight(),
|
modifier = Modifier.width(contentWidth).fillMaxHeight(),
|
||||||
verticalArrangement = Arrangement.spacedBy(if (compact) 4.dp else 6.dp),
|
verticalArrangement = Arrangement.spacedBy(if (compact) 5.dp else 8.dp),
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
sectionLabel.uppercase(),
|
sectionLabel.uppercase(),
|
||||||
@@ -1626,7 +1627,10 @@ private fun MetadataStatus(item: BaseItem) {
|
|||||||
internal val HomeRowHeaderIconGap = 10.dp
|
internal val HomeRowHeaderIconGap = 10.dp
|
||||||
|
|
||||||
/** Vertical gap between a row's header and its cards. One value for every row. */
|
/** Vertical gap between a row's header and its cards. One value for every row. */
|
||||||
internal val HomeRowHeaderSpacing = 6.dp
|
internal val HomeRowHeaderSpacing = 10.dp
|
||||||
|
|
||||||
|
private val HomeRowCardsTopPadding = 12.dp
|
||||||
|
private val HomeRowCardsBottomPadding = 24.dp
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
internal fun HomeRowHeaderIcon(icon: ImageVector, modifier: Modifier = Modifier) {
|
internal fun HomeRowHeaderIcon(icon: ImageVector, modifier: Modifier = Modifier) {
|
||||||
@@ -1694,6 +1698,31 @@ fun MediaBadge(label: String, modifier: Modifier = Modifier) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal fun recommendationReasonSummary(reason: String): String =
|
||||||
|
reason
|
||||||
|
.split('·', '•')
|
||||||
|
.asSequence()
|
||||||
|
.map(String::trim)
|
||||||
|
.firstOrNull(String::isNotEmpty)
|
||||||
|
?: reason.trim()
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
internal fun RecommendationReasonCaption(
|
||||||
|
text: String,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = text,
|
||||||
|
color = EmbyGreen.copy(alpha = 0.88f),
|
||||||
|
fontSize = 12.sp,
|
||||||
|
lineHeight = 16.sp,
|
||||||
|
fontWeight = FontWeight.Medium,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
modifier = modifier,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
internal fun mediaBadges(item: BaseItem): List<String> {
|
internal fun mediaBadges(item: BaseItem): List<String> {
|
||||||
val video = item.mediaStreams.firstOrNull { it.type.equals("Video", true) }
|
val video = item.mediaStreams.firstOrNull { it.type.equals("Video", true) }
|
||||||
val audio = item.mediaStreams.firstOrNull { it.type.equals("Audio", true) }
|
val audio = item.mediaStreams.firstOrNull { it.type.equals("Audio", true) }
|
||||||
@@ -1774,7 +1803,7 @@ internal fun MediaRow(
|
|||||||
) 6 else 4
|
) 6 else 4
|
||||||
Column(modifier, verticalArrangement = Arrangement.spacedBy(HomeRowHeaderSpacing)) {
|
Column(modifier, verticalArrangement = Arrangement.spacedBy(HomeRowHeaderSpacing)) {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.fillMaxWidth().padding(horizontal = 36.dp),
|
modifier = Modifier.fillMaxWidth().padding(horizontal = HomeContentHorizontalInset),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
) {
|
) {
|
||||||
val visual = homeRowVisual(row)
|
val visual = homeRowVisual(row)
|
||||||
@@ -1811,7 +1840,12 @@ internal fun MediaRow(
|
|||||||
when {
|
when {
|
||||||
row.items.isEmpty() && row.loading -> {
|
row.items.isEmpty() && row.loading -> {
|
||||||
LazyRow(
|
LazyRow(
|
||||||
contentPadding = PaddingValues(horizontal = 36.dp, vertical = 9.dp),
|
contentPadding = PaddingValues(
|
||||||
|
start = HomeContentHorizontalInset,
|
||||||
|
end = HomeContentHorizontalInset,
|
||||||
|
top = HomeRowCardsTopPadding,
|
||||||
|
bottom = HomeRowCardsBottomPadding,
|
||||||
|
),
|
||||||
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
||||||
) {
|
) {
|
||||||
items(5) {
|
items(5) {
|
||||||
@@ -1827,13 +1861,21 @@ internal fun MediaRow(
|
|||||||
row.emptyMessage,
|
row.emptyMessage,
|
||||||
color = QuietText,
|
color = QuietText,
|
||||||
fontSize = 14.sp,
|
fontSize = 14.sp,
|
||||||
modifier = Modifier.padding(horizontal = 36.dp, vertical = 18.dp),
|
modifier = Modifier.padding(
|
||||||
|
horizontal = HomeContentHorizontalInset,
|
||||||
|
vertical = 18.dp,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
LazyRow(
|
LazyRow(
|
||||||
state = rowState,
|
state = rowState,
|
||||||
contentPadding = PaddingValues(horizontal = 36.dp, vertical = 10.dp),
|
contentPadding = PaddingValues(
|
||||||
|
start = HomeContentHorizontalInset,
|
||||||
|
end = HomeContentHorizontalInset,
|
||||||
|
top = HomeRowCardsTopPadding,
|
||||||
|
bottom = HomeRowCardsBottomPadding,
|
||||||
|
),
|
||||||
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
||||||
// focusRestorer pins a lazy item. Replacing a For You result set
|
// focusRestorer pins a lazy item. Replacing a For You result set
|
||||||
// can dispose that item during a focus transfer and make Compose
|
// can dispose that item during a focus transfer and make Compose
|
||||||
@@ -1956,16 +1998,15 @@ private fun GalleryJumpButton(
|
|||||||
|
|
||||||
private enum class MediaCardFormat { PORTRAIT, LANDSCAPE }
|
private enum class MediaCardFormat { PORTRAIT, LANDSCAPE }
|
||||||
|
|
||||||
private val MediaRowHorizontalInset = 36.dp
|
private val MediaRowHorizontalInset = HomeContentHorizontalInset
|
||||||
private val MediaRowCardSpacing = 16.dp
|
private val MediaRowCardSpacing = 16.dp
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sizes a shelf to an integer number of complete cards.
|
* Sizes a shelf to an integer number of complete cards.
|
||||||
*
|
*
|
||||||
* The old fractional "cards across" values intentionally left the next card hanging
|
* Work from the actual row width, including the launcher inset, so the first card, the
|
||||||
* outside the viewport. Besides looking clipped, that became more pronounced on smaller
|
* heading and the metadata panel stay on one column while the opposite inset still leaves
|
||||||
* TVs and whenever the navigation rail reduced the content width. Work from the actual
|
* a deliberate sliver of the next card visible as a browse cue.
|
||||||
* row width (including its padding and gaps) so every visible card has a complete edge.
|
|
||||||
*/
|
*/
|
||||||
internal fun responsiveRowCardWidth(
|
internal fun responsiveRowCardWidth(
|
||||||
availableWidth: Dp,
|
availableWidth: Dp,
|
||||||
@@ -2187,14 +2228,14 @@ private fun MediaCard(
|
|||||||
// when a row is moving into view, which shows up as a small hitch on
|
// when a row is moving into view, which shows up as a small hitch on
|
||||||
// lower-powered TV hardware.
|
// lower-powered TV hardware.
|
||||||
.shadow(
|
.shadow(
|
||||||
elevation = if (focused) 7.dp else 0.dp,
|
elevation = if (focused) 5.dp else 0.dp,
|
||||||
shape = RoundedCornerShape(MembyCardCorner),
|
shape = RoundedCornerShape(MembyCardCorner),
|
||||||
)
|
)
|
||||||
.clip(RoundedCornerShape(MembyCardCorner))
|
.clip(RoundedCornerShape(MembyCardCorner))
|
||||||
.background(MembySurfaceRaised)
|
.background(MembySurfaceRaised)
|
||||||
.border(
|
.border(
|
||||||
2.dp,
|
if (focused) 1.5.dp else 1.dp,
|
||||||
if (focused) Color.White else Color.White.copy(alpha = 0.07f),
|
if (focused) Color.White.copy(alpha = 0.76f) else Color.White.copy(alpha = 0.06f),
|
||||||
RoundedCornerShape(MembyCardCorner),
|
RoundedCornerShape(MembyCardCorner),
|
||||||
),
|
),
|
||||||
contentAlignment = Alignment.Center,
|
contentAlignment = Alignment.Center,
|
||||||
@@ -2293,7 +2334,7 @@ private fun MediaCard(
|
|||||||
fontWeight = if (focused) FontWeight.Bold else FontWeight.SemiBold,
|
fontWeight = if (focused) FontWeight.Bold else FontWeight.SemiBold,
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
overflow = TextOverflow.Ellipsis,
|
overflow = TextOverflow.Ellipsis,
|
||||||
modifier = Modifier.padding(top = 7.dp).fillMaxWidth(),
|
modifier = Modifier.padding(top = 9.dp).fillMaxWidth(),
|
||||||
)
|
)
|
||||||
if (showSecondaryMetadata) {
|
if (showSecondaryMetadata) {
|
||||||
// The media type sits *in* the metadata line rather than over the poster:
|
// The media type sits *in* the metadata line rather than over the poster:
|
||||||
@@ -2304,7 +2345,7 @@ private fun MediaCard(
|
|||||||
val typeMark = if (showMediaTypeIcon) mediaTypeMark(item) else null
|
val typeMark = if (showMediaTypeIcon) mediaTypeMark(item) else null
|
||||||
Row(
|
Row(
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
modifier = Modifier.padding(top = 2.dp).fillMaxWidth(),
|
modifier = Modifier.padding(top = 4.dp).fillMaxWidth(),
|
||||||
) {
|
) {
|
||||||
typeMark?.let { (icon, _) ->
|
typeMark?.let { (icon, _) ->
|
||||||
Icon(
|
Icon(
|
||||||
@@ -2328,7 +2369,7 @@ private fun MediaCard(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Spacer(Modifier.height(3.dp))
|
Spacer(Modifier.height(5.dp))
|
||||||
}
|
}
|
||||||
// No ratings strip under a poster. The scores are on the detail page the card
|
// No ratings strip under a poster. The scores are on the detail page the card
|
||||||
// opens and in the metadata panel beside the focused card; a third copy under
|
// opens and in the metadata panel beside the focused card; a third copy under
|
||||||
@@ -2444,7 +2485,7 @@ fun FocusScaleContainer(
|
|||||||
) {
|
) {
|
||||||
var focused by remember { mutableStateOf(false) }
|
var focused by remember { mutableStateOf(false) }
|
||||||
val scale = animateFloatAsState(
|
val scale = animateFloatAsState(
|
||||||
targetValue = if (focused) 1.025f else 1f,
|
targetValue = if (focused) 1.018f else 1f,
|
||||||
animationSpec = tween(95),
|
animationSpec = tween(95),
|
||||||
label = "media-card-focus",
|
label = "media-card-focus",
|
||||||
)
|
)
|
||||||
@@ -2543,8 +2584,7 @@ private fun cardSubtitle(
|
|||||||
else -> defaultCardSubtitle(item)
|
else -> defaultCardSubtitle(item)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun defaultCardSubtitle(item: BaseItem): String = when {
|
internal fun defaultCardSubtitle(item: BaseItem): String = when {
|
||||||
!item.membyRecommendationReason.isNullOrBlank() -> item.membyRecommendationReason
|
|
||||||
item.isSchedule -> item.membyAirLabel ?: "Coming up"
|
item.isSchedule -> item.membyAirLabel ?: "Coming up"
|
||||||
item.isEpisode -> item.seriesName ?: "Up next"
|
item.isEpisode -> item.seriesName ?: "Up next"
|
||||||
item.productionYear != null && item.runtimeMinutes != null ->
|
item.productionYear != null && item.runtimeMinutes != null ->
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import androidx.compose.foundation.layout.height
|
|||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.layout.widthIn
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
@@ -281,8 +282,15 @@ internal fun HomeMovieHero(
|
|||||||
BoxWithConstraints(modifier.fillMaxWidth()) {
|
BoxWithConstraints(modifier.fillMaxWidth()) {
|
||||||
val miniWidth = (maxWidth * 0.27f).coerceIn(184.dp, 326.dp)
|
val miniWidth = (maxWidth * 0.27f).coerceIn(184.dp, 326.dp)
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.fillMaxSize().padding(start = 36.dp, end = 36.dp, top = 16.dp, bottom = 10.dp),
|
modifier = Modifier
|
||||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
.fillMaxSize()
|
||||||
|
.padding(
|
||||||
|
start = HomeContentHorizontalInset,
|
||||||
|
end = HomeContentHorizontalInset,
|
||||||
|
top = 20.dp,
|
||||||
|
bottom = 18.dp,
|
||||||
|
),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
||||||
) {
|
) {
|
||||||
val featured = movies.first()
|
val featured = movies.first()
|
||||||
// Down is stated rather than left to Compose's spatial search. The hero cards
|
// Down is stated rather than left to Compose's spatial search. The hero cards
|
||||||
@@ -402,9 +410,10 @@ private fun FeaturedMovieCard(
|
|||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.align(Alignment.CenterStart)
|
.align(Alignment.CenterStart)
|
||||||
.fillMaxWidth(0.58f)
|
.fillMaxWidth(0.6f)
|
||||||
|
.widthIn(max = 560.dp)
|
||||||
.fillMaxHeight()
|
.fillMaxHeight()
|
||||||
.padding(22.dp),
|
.padding(start = 26.dp, end = 20.dp, top = 24.dp, bottom = 24.dp),
|
||||||
verticalArrangement = Arrangement.Center,
|
verticalArrangement = Arrangement.Center,
|
||||||
) {
|
) {
|
||||||
Column(Modifier.weight(1f, fill = false)) {
|
Column(Modifier.weight(1f, fill = false)) {
|
||||||
@@ -449,27 +458,25 @@ private fun FeaturedMovieCard(
|
|||||||
// the launcher was silently dropped on exactly the long-titled films
|
// the launcher was silently dropped on exactly the long-titled films
|
||||||
// most likely to be leading it. The scores are also on the detail page
|
// most likely to be leading it. The scores are also on the detail page
|
||||||
// this card opens; the reason is not anywhere else.
|
// this card opens; the reason is not anywhere else.
|
||||||
val reason = pick.reason?.takeIf(String::isNotBlank)
|
val reason = pick.reason
|
||||||
|
?.takeIf(String::isNotBlank)
|
||||||
|
?.let(::recommendationReasonSummary)
|
||||||
if (reason != null) {
|
if (reason != null) {
|
||||||
Spacer(Modifier.height(7.dp))
|
Spacer(Modifier.height(10.dp))
|
||||||
Text(
|
RecommendationReasonCaption(
|
||||||
reason,
|
text = reason,
|
||||||
color = MembyAccent,
|
modifier = Modifier.fillMaxWidth(0.9f),
|
||||||
fontSize = 13.sp,
|
|
||||||
lineHeight = 17.sp,
|
|
||||||
fontWeight = FontWeight.SemiBold,
|
|
||||||
maxLines = 1,
|
|
||||||
overflow = TextOverflow.Ellipsis,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
ItemRatingsStrip(
|
ItemRatingsStrip(
|
||||||
item = item,
|
item = item,
|
||||||
load = true,
|
load = true,
|
||||||
modifier = Modifier.padding(top = 6.dp).fillMaxWidth(),
|
compact = true,
|
||||||
|
modifier = Modifier.padding(top = 10.dp).fillMaxWidth(0.88f),
|
||||||
)
|
)
|
||||||
if (reason == null && titleLines == 1) {
|
if (reason == null && titleLines == 1) {
|
||||||
item.overview?.takeIf(String::isNotBlank)?.let { overview ->
|
item.overview?.takeIf(String::isNotBlank)?.let { overview ->
|
||||||
Spacer(Modifier.height(9.dp))
|
Spacer(Modifier.height(11.dp))
|
||||||
Text(
|
Text(
|
||||||
overview,
|
overview,
|
||||||
color = MembyMutedText,
|
color = MembyMutedText,
|
||||||
|
|||||||
@@ -1396,7 +1396,10 @@ internal fun HomeScreen(
|
|||||||
homeStatus.statusMessage ?: "Connection is slow — showing available content",
|
homeStatus.statusMessage ?: "Connection is slow — showing available content",
|
||||||
color = MembyMutedText,
|
color = MembyMutedText,
|
||||||
fontSize = 14.sp,
|
fontSize = 14.sp,
|
||||||
modifier = Modifier.padding(horizontal = 36.dp, vertical = 4.dp),
|
modifier = Modifier.padding(
|
||||||
|
horizontal = HomeContentHorizontalInset,
|
||||||
|
vertical = 6.dp,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
@@ -1404,8 +1407,8 @@ internal fun HomeScreen(
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.weight(1f)
|
.weight(1f)
|
||||||
.fillMaxWidth(),
|
.fillMaxWidth(),
|
||||||
contentPadding = PaddingValues(bottom = 96.dp),
|
contentPadding = PaddingValues(bottom = 128.dp),
|
||||||
verticalArrangement = Arrangement.spacedBy(14.dp),
|
verticalArrangement = Arrangement.spacedBy(20.dp),
|
||||||
) {
|
) {
|
||||||
if (selectedDestination == BrowseDestination.SHOWS) {
|
if (selectedDestination == BrowseDestination.SHOWS) {
|
||||||
item(key = "my-shows", contentType = "my-shows") {
|
item(key = "my-shows", contentType = "my-shows") {
|
||||||
|
|||||||
@@ -197,10 +197,16 @@ class HomeMovieHeroScreenshotTest {
|
|||||||
"Matt's Favourites",
|
"Matt's Favourites",
|
||||||
color = Color(0xFFF1F3F4),
|
color = Color(0xFFF1F3F4),
|
||||||
fontSize = 18.sp,
|
fontSize = 18.sp,
|
||||||
modifier = Modifier.padding(start = 36.dp, top = 8.dp, bottom = 10.dp),
|
modifier = Modifier.padding(
|
||||||
|
start = HomeContentHorizontalInset,
|
||||||
|
top = 10.dp,
|
||||||
|
bottom = 12.dp,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.fillMaxWidth().padding(horizontal = 36.dp),
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = HomeContentHorizontalInset),
|
||||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||||
) {
|
) {
|
||||||
repeat(5) { index ->
|
repeat(5) { index ->
|
||||||
@@ -238,6 +244,50 @@ class HomeMovieHeroScreenshotTest {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `metadata hero keeps recommendation reasons secondary`() {
|
||||||
|
val artwork = requireNotNull(javaClass.getResourceAsStream("/home_hero_preview_art.png"))
|
||||||
|
.use(BitmapFactory::decodeStream)
|
||||||
|
.asImageBitmap()
|
||||||
|
val item = movie(
|
||||||
|
"The Last Horizon",
|
||||||
|
2026,
|
||||||
|
"Beyond the mapped worlds, one explorer finds an ocean that remembers every visitor.",
|
||||||
|
8.7,
|
||||||
|
).copy(
|
||||||
|
membyRecommendationReason = "Matches your Drama viewing · fits your 60-minute window",
|
||||||
|
membyRatings = listOf(
|
||||||
|
MediaRating(source = "imdb", name = "IMDb", score = "8.7", scale = "/10"),
|
||||||
|
MediaRating(source = "tomatoes", name = "Rotten Tomatoes", score = "93", scale = "%"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
compose.setContent {
|
||||||
|
PreviewSurface(alignment = Alignment.TopStart) {
|
||||||
|
Box(Modifier.fillMaxSize()) {
|
||||||
|
Image(
|
||||||
|
bitmap = artwork,
|
||||||
|
contentDescription = null,
|
||||||
|
contentScale = ContentScale.Crop,
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
)
|
||||||
|
MetadataHero(
|
||||||
|
item = item,
|
||||||
|
loading = false,
|
||||||
|
sectionLabel = "For You",
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(homeHeaderHeight(540.dp, showHero = false)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
compose.onRoot().captureRoboImage(
|
||||||
|
"build/screenshots/home-movie-hero/metadata-hero-recommendation-reason.png",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
private fun capture(name: String, movies: List<HomeHeroPick>) {
|
private fun capture(name: String, movies: List<HomeHeroPick>) {
|
||||||
val artwork = requireNotNull(javaClass.getResourceAsStream("/home_hero_preview_art.png"))
|
val artwork = requireNotNull(javaClass.getResourceAsStream("/home_hero_preview_art.png"))
|
||||||
.use(BitmapFactory::decodeStream)
|
.use(BitmapFactory::decodeStream)
|
||||||
@@ -267,10 +317,16 @@ class HomeMovieHeroScreenshotTest {
|
|||||||
"Recently added movies",
|
"Recently added movies",
|
||||||
color = Color(0xFFF1F3F4),
|
color = Color(0xFFF1F3F4),
|
||||||
fontSize = 18.sp,
|
fontSize = 18.sp,
|
||||||
modifier = Modifier.padding(start = 36.dp, top = 6.dp, bottom = 9.dp),
|
modifier = Modifier.padding(
|
||||||
|
start = HomeContentHorizontalInset,
|
||||||
|
top = 10.dp,
|
||||||
|
bottom = 12.dp,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.fillMaxWidth().padding(horizontal = 36.dp),
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = HomeContentHorizontalInset),
|
||||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||||
) {
|
) {
|
||||||
repeat(5) { index ->
|
repeat(5) { index ->
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ class ResponsiveRowSizingTest {
|
|||||||
@Test
|
@Test
|
||||||
fun metadataPanelKeepsReadableLineLengthsAcrossTvWidths() {
|
fun metadataPanelKeepsReadableLineLengthsAcrossTvWidths() {
|
||||||
assertEquals(537.6f, metadataPanelContentWidth(640.dp, compact = true).value, 0.01f)
|
assertEquals(537.6f, metadataPanelContentWidth(640.dp, compact = true).value, 0.01f)
|
||||||
assertEquals(720f, metadataPanelContentWidth(1920.dp, compact = false).value, 0.01f)
|
assertEquals(680f, metadataPanelContentWidth(1920.dp, compact = false).value, 0.01f)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -21,7 +21,7 @@ class ResponsiveRowSizingTest {
|
|||||||
maxWidth = 218.dp,
|
maxWidth = 218.dp,
|
||||||
)
|
)
|
||||||
|
|
||||||
val occupiedWidth = width * 7 + 16.dp * 6 + 36.dp * 2
|
val occupiedWidth = width * 7 + 16.dp * 6 + HomeContentHorizontalInset * 2
|
||||||
assertEquals(1280f, occupiedWidth.value, 0.01f)
|
assertEquals(1280f, occupiedWidth.value, 0.01f)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -34,9 +34,9 @@ class ResponsiveRowSizingTest {
|
|||||||
maxWidth = 218.dp,
|
maxWidth = 218.dp,
|
||||||
)
|
)
|
||||||
|
|
||||||
// Four 130dp cards, three gaps and both insets fill this viewport exactly.
|
// Four 124dp cards, three gaps and both insets fill this viewport exactly.
|
||||||
assertEquals(130f, width.value, 0.01f)
|
assertEquals(124f, width.value, 0.01f)
|
||||||
assertEquals(640f, (width * 4 + 16.dp * 3 + 36.dp * 2).value, 0.01f)
|
assertEquals(640f, (width * 4 + 16.dp * 3 + HomeContentHorizontalInset * 2).value, 0.01f)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -49,6 +49,30 @@ class ResponsiveRowSizingTest {
|
|||||||
)
|
)
|
||||||
|
|
||||||
assertTrue(width <= 360.dp)
|
assertTrue(width <= 360.dp)
|
||||||
assertEquals(341.71f, width.value, 0.01f)
|
assertEquals(338.29f, width.value, 0.01f)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun cardSubtitlePrefersFactsOverRecommendationReason() {
|
||||||
|
val item = BaseItem(
|
||||||
|
id = "movie-1",
|
||||||
|
name = "The Last Horizon",
|
||||||
|
type = "Movie",
|
||||||
|
productionYear = 2026,
|
||||||
|
runtimeMinutes = 124,
|
||||||
|
membyRecommendationReason = "Matches your Drama viewing · fits your 60-minute window",
|
||||||
|
)
|
||||||
|
|
||||||
|
assertEquals("2026 • 2h 4m", defaultCardSubtitle(item))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun recommendationReasonSummaryKeepsTheFirstUsefulClause() {
|
||||||
|
assertEquals(
|
||||||
|
"Matches your Drama viewing",
|
||||||
|
recommendationReasonSummary(
|
||||||
|
"Matches your Drama viewing · fits your 60-minute window",
|
||||||
|
),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user