0.2.89
This commit is contained in:
@@ -140,6 +140,7 @@ private val QuietText: Color get() = MembyQuietText
|
||||
internal val TvRailCollapsedWidth = 54.dp
|
||||
internal val TvRailExpandedWidth = 184.dp
|
||||
internal val TvRailContentShift = 112.dp
|
||||
internal val HomeContentHorizontalInset = 48.dp
|
||||
|
||||
/**
|
||||
* The rail's order, which is this declaration order — see [navigationRailItems], the only
|
||||
@@ -1136,14 +1137,19 @@ internal fun MetadataHero(
|
||||
sectionLabel = sectionLabel,
|
||||
modifier = Modifier
|
||||
.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 =
|
||||
(availableWidth * if (compact) 0.84f else 0.76f)
|
||||
.coerceIn(280.dp, 720.dp)
|
||||
.coerceIn(280.dp, 680.dp)
|
||||
.coerceAtMost(availableWidth)
|
||||
|
||||
@Composable
|
||||
@@ -1436,7 +1442,7 @@ private fun MetadataContent(
|
||||
}
|
||||
Column(
|
||||
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(
|
||||
@@ -1473,7 +1479,7 @@ private fun MetadataContent(
|
||||
ItemRatingsStrip(
|
||||
item = item,
|
||||
load = true,
|
||||
compact = compact,
|
||||
compact = true,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
val badges = buildList {
|
||||
@@ -1481,13 +1487,8 @@ private fun MetadataContent(
|
||||
addAll(mediaBadges(item))
|
||||
}
|
||||
item.membyRecommendationReason?.takeIf(String::isNotBlank)?.let {
|
||||
Text(
|
||||
text = it,
|
||||
color = EmbyGreen,
|
||||
fontSize = 13.sp,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
maxLines = 2,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
RecommendationReasonCaption(
|
||||
text = recommendationReasonSummary(it),
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
}
|
||||
@@ -1518,7 +1519,7 @@ private fun ScheduleMetadataContent(
|
||||
) {
|
||||
Column(
|
||||
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(),
|
||||
@@ -1626,7 +1627,10 @@ private fun MetadataStatus(item: BaseItem) {
|
||||
internal val HomeRowHeaderIconGap = 10.dp
|
||||
|
||||
/** 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
|
||||
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> {
|
||||
val video = item.mediaStreams.firstOrNull { it.type.equals("Video", true) }
|
||||
val audio = item.mediaStreams.firstOrNull { it.type.equals("Audio", true) }
|
||||
@@ -1774,7 +1803,7 @@ internal fun MediaRow(
|
||||
) 6 else 4
|
||||
Column(modifier, verticalArrangement = Arrangement.spacedBy(HomeRowHeaderSpacing)) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth().padding(horizontal = 36.dp),
|
||||
modifier = Modifier.fillMaxWidth().padding(horizontal = HomeContentHorizontalInset),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
val visual = homeRowVisual(row)
|
||||
@@ -1811,7 +1840,12 @@ internal fun MediaRow(
|
||||
when {
|
||||
row.items.isEmpty() && row.loading -> {
|
||||
LazyRow(
|
||||
contentPadding = PaddingValues(horizontal = 36.dp, vertical = 9.dp),
|
||||
contentPadding = PaddingValues(
|
||||
start = HomeContentHorizontalInset,
|
||||
end = HomeContentHorizontalInset,
|
||||
top = HomeRowCardsTopPadding,
|
||||
bottom = HomeRowCardsBottomPadding,
|
||||
),
|
||||
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
||||
) {
|
||||
items(5) {
|
||||
@@ -1827,13 +1861,21 @@ internal fun MediaRow(
|
||||
row.emptyMessage,
|
||||
color = QuietText,
|
||||
fontSize = 14.sp,
|
||||
modifier = Modifier.padding(horizontal = 36.dp, vertical = 18.dp),
|
||||
modifier = Modifier.padding(
|
||||
horizontal = HomeContentHorizontalInset,
|
||||
vertical = 18.dp,
|
||||
),
|
||||
)
|
||||
}
|
||||
else -> {
|
||||
LazyRow(
|
||||
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),
|
||||
// focusRestorer pins a lazy item. Replacing a For You result set
|
||||
// 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 val MediaRowHorizontalInset = 36.dp
|
||||
private val MediaRowHorizontalInset = HomeContentHorizontalInset
|
||||
private val MediaRowCardSpacing = 16.dp
|
||||
|
||||
/**
|
||||
* Sizes a shelf to an integer number of complete cards.
|
||||
*
|
||||
* The old fractional "cards across" values intentionally left the next card hanging
|
||||
* outside the viewport. Besides looking clipped, that became more pronounced on smaller
|
||||
* TVs and whenever the navigation rail reduced the content width. Work from the actual
|
||||
* row width (including its padding and gaps) so every visible card has a complete edge.
|
||||
* Work from the actual row width, including the launcher inset, so the first card, the
|
||||
* heading and the metadata panel stay on one column while the opposite inset still leaves
|
||||
* a deliberate sliver of the next card visible as a browse cue.
|
||||
*/
|
||||
internal fun responsiveRowCardWidth(
|
||||
availableWidth: Dp,
|
||||
@@ -2187,14 +2228,14 @@ private fun MediaCard(
|
||||
// when a row is moving into view, which shows up as a small hitch on
|
||||
// lower-powered TV hardware.
|
||||
.shadow(
|
||||
elevation = if (focused) 7.dp else 0.dp,
|
||||
elevation = if (focused) 5.dp else 0.dp,
|
||||
shape = RoundedCornerShape(MembyCardCorner),
|
||||
)
|
||||
.clip(RoundedCornerShape(MembyCardCorner))
|
||||
.background(MembySurfaceRaised)
|
||||
.border(
|
||||
2.dp,
|
||||
if (focused) Color.White else Color.White.copy(alpha = 0.07f),
|
||||
if (focused) 1.5.dp else 1.dp,
|
||||
if (focused) Color.White.copy(alpha = 0.76f) else Color.White.copy(alpha = 0.06f),
|
||||
RoundedCornerShape(MembyCardCorner),
|
||||
),
|
||||
contentAlignment = Alignment.Center,
|
||||
@@ -2293,7 +2334,7 @@ private fun MediaCard(
|
||||
fontWeight = if (focused) FontWeight.Bold else FontWeight.SemiBold,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier.padding(top = 7.dp).fillMaxWidth(),
|
||||
modifier = Modifier.padding(top = 9.dp).fillMaxWidth(),
|
||||
)
|
||||
if (showSecondaryMetadata) {
|
||||
// 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
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.padding(top = 2.dp).fillMaxWidth(),
|
||||
modifier = Modifier.padding(top = 4.dp).fillMaxWidth(),
|
||||
) {
|
||||
typeMark?.let { (icon, _) ->
|
||||
Icon(
|
||||
@@ -2328,7 +2369,7 @@ private fun MediaCard(
|
||||
)
|
||||
}
|
||||
} 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
|
||||
// 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) }
|
||||
val scale = animateFloatAsState(
|
||||
targetValue = if (focused) 1.025f else 1f,
|
||||
targetValue = if (focused) 1.018f else 1f,
|
||||
animationSpec = tween(95),
|
||||
label = "media-card-focus",
|
||||
)
|
||||
@@ -2543,8 +2584,7 @@ private fun cardSubtitle(
|
||||
else -> defaultCardSubtitle(item)
|
||||
}
|
||||
|
||||
private fun defaultCardSubtitle(item: BaseItem): String = when {
|
||||
!item.membyRecommendationReason.isNullOrBlank() -> item.membyRecommendationReason
|
||||
internal fun defaultCardSubtitle(item: BaseItem): String = when {
|
||||
item.isSchedule -> item.membyAirLabel ?: "Coming up"
|
||||
item.isEpisode -> item.seriesName ?: "Up next"
|
||||
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.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.layout.widthIn
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
@@ -281,8 +282,15 @@ internal fun HomeMovieHero(
|
||||
BoxWithConstraints(modifier.fillMaxWidth()) {
|
||||
val miniWidth = (maxWidth * 0.27f).coerceIn(184.dp, 326.dp)
|
||||
Row(
|
||||
modifier = Modifier.fillMaxSize().padding(start = 36.dp, end = 36.dp, top = 16.dp, bottom = 10.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(
|
||||
start = HomeContentHorizontalInset,
|
||||
end = HomeContentHorizontalInset,
|
||||
top = 20.dp,
|
||||
bottom = 18.dp,
|
||||
),
|
||||
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
||||
) {
|
||||
val featured = movies.first()
|
||||
// Down is stated rather than left to Compose's spatial search. The hero cards
|
||||
@@ -402,9 +410,10 @@ private fun FeaturedMovieCard(
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.align(Alignment.CenterStart)
|
||||
.fillMaxWidth(0.58f)
|
||||
.fillMaxWidth(0.6f)
|
||||
.widthIn(max = 560.dp)
|
||||
.fillMaxHeight()
|
||||
.padding(22.dp),
|
||||
.padding(start = 26.dp, end = 20.dp, top = 24.dp, bottom = 24.dp),
|
||||
verticalArrangement = Arrangement.Center,
|
||||
) {
|
||||
Column(Modifier.weight(1f, fill = false)) {
|
||||
@@ -449,27 +458,25 @@ private fun FeaturedMovieCard(
|
||||
// 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
|
||||
// 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) {
|
||||
Spacer(Modifier.height(7.dp))
|
||||
Text(
|
||||
reason,
|
||||
color = MembyAccent,
|
||||
fontSize = 13.sp,
|
||||
lineHeight = 17.sp,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
Spacer(Modifier.height(10.dp))
|
||||
RecommendationReasonCaption(
|
||||
text = reason,
|
||||
modifier = Modifier.fillMaxWidth(0.9f),
|
||||
)
|
||||
}
|
||||
ItemRatingsStrip(
|
||||
item = item,
|
||||
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) {
|
||||
item.overview?.takeIf(String::isNotBlank)?.let { overview ->
|
||||
Spacer(Modifier.height(9.dp))
|
||||
Spacer(Modifier.height(11.dp))
|
||||
Text(
|
||||
overview,
|
||||
color = MembyMutedText,
|
||||
|
||||
@@ -1396,7 +1396,10 @@ internal fun HomeScreen(
|
||||
homeStatus.statusMessage ?: "Connection is slow — showing available content",
|
||||
color = MembyMutedText,
|
||||
fontSize = 14.sp,
|
||||
modifier = Modifier.padding(horizontal = 36.dp, vertical = 4.dp),
|
||||
modifier = Modifier.padding(
|
||||
horizontal = HomeContentHorizontalInset,
|
||||
vertical = 6.dp,
|
||||
),
|
||||
)
|
||||
}
|
||||
LazyColumn(
|
||||
@@ -1404,8 +1407,8 @@ internal fun HomeScreen(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.fillMaxWidth(),
|
||||
contentPadding = PaddingValues(bottom = 96.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(14.dp),
|
||||
contentPadding = PaddingValues(bottom = 128.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(20.dp),
|
||||
) {
|
||||
if (selectedDestination == BrowseDestination.SHOWS) {
|
||||
item(key = "my-shows", contentType = "my-shows") {
|
||||
|
||||
@@ -197,10 +197,16 @@ class HomeMovieHeroScreenshotTest {
|
||||
"Matt's Favourites",
|
||||
color = Color(0xFFF1F3F4),
|
||||
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(
|
||||
modifier = Modifier.fillMaxWidth().padding(horizontal = 36.dp),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = HomeContentHorizontalInset),
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
) {
|
||||
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>) {
|
||||
val artwork = requireNotNull(javaClass.getResourceAsStream("/home_hero_preview_art.png"))
|
||||
.use(BitmapFactory::decodeStream)
|
||||
@@ -267,10 +317,16 @@ class HomeMovieHeroScreenshotTest {
|
||||
"Recently added movies",
|
||||
color = Color(0xFFF1F3F4),
|
||||
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(
|
||||
modifier = Modifier.fillMaxWidth().padding(horizontal = 36.dp),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = HomeContentHorizontalInset),
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
) {
|
||||
repeat(5) { index ->
|
||||
|
||||
@@ -9,7 +9,7 @@ class ResponsiveRowSizingTest {
|
||||
@Test
|
||||
fun metadataPanelKeepsReadableLineLengthsAcrossTvWidths() {
|
||||
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
|
||||
@@ -21,7 +21,7 @@ class ResponsiveRowSizingTest {
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -34,9 +34,9 @@ class ResponsiveRowSizingTest {
|
||||
maxWidth = 218.dp,
|
||||
)
|
||||
|
||||
// Four 130dp cards, three gaps and both insets fill this viewport exactly.
|
||||
assertEquals(130f, width.value, 0.01f)
|
||||
assertEquals(640f, (width * 4 + 16.dp * 3 + 36.dp * 2).value, 0.01f)
|
||||
// Four 124dp cards, three gaps and both insets fill this viewport exactly.
|
||||
assertEquals(124f, width.value, 0.01f)
|
||||
assertEquals(640f, (width * 4 + 16.dp * 3 + HomeContentHorizontalInset * 2).value, 0.01f)
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -49,6 +49,30 @@ class ResponsiveRowSizingTest {
|
||||
)
|
||||
|
||||
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