This commit is contained in:
ponzischeme89
2026-08-21 16:02:25 +12:00
parent 4439277c86
commit 891d3dfedc
12 changed files with 88 additions and 79 deletions
+4
View File
@@ -1,3 +1,7 @@
## 0.2.92 - 2026-08-21
- Improved: App design / layout tweaks.
- Improved: Many behind the scenes tweaks, bug fixes to make the app run smoother.
## 0.2.84 - 2026-08-21 ## 0.2.84 - 2026-08-21
- Improved: Loading and performance of the continue watching row. - Improved: Loading and performance of the continue watching row.
+1 -1
View File
@@ -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.91" val defaultVersionName = "0.2.92"
val membyVersionName: String = val membyVersionName: String =
(project.findProperty("memby.versionName") as String?) (project.findProperty("memby.versionName") as String?)
?.trim() ?.trim()
@@ -40,13 +40,7 @@ internal fun MetadataHero(
Box( Box(
modifier = modifier modifier = modifier
.clipToBounds() .clipToBounds()
.background( .background(MembySurface),
Brush.verticalGradient(
0f to MembySurface.copy(alpha = 0.74f),
0.70f to MembySurface.copy(alpha = 0.52f),
1f to MembySurface.copy(alpha = 0.18f),
),
),
) { ) {
MetadataHeroArtwork( MetadataHeroArtwork(
item = item, item = item,
@@ -55,20 +49,21 @@ internal fun MetadataHero(
Box( Box(
Modifier.fillMaxSize().background( Modifier.fillMaxSize().background(
Brush.horizontalGradient( Brush.horizontalGradient(
0f to MembySurface.copy(alpha = 0.94f), 0f to MembySurface,
0.34f to MembySurface.copy(alpha = 0.90f), 0.30f to MembySurface,
0.60f to MembySurface.copy(alpha = 0.74f), 0.48f to MembySurface.copy(alpha = 0.80f),
0.82f to MembySurface.copy(alpha = 0.38f), 0.62f to MembySurface.copy(alpha = 0.40f),
1f to MembySurface.copy(alpha = 0.18f), 0.78f to MembySurface.copy(alpha = 0.10f),
1f to Color.Transparent,
), ),
), ),
) )
Box( Box(
Modifier.fillMaxSize().background( Modifier.fillMaxSize().background(
Brush.verticalGradient( Brush.verticalGradient(
0f to MembySurface.copy(alpha = 0.28f), 0f to MembySurface.copy(alpha = 0.18f),
0.74f to Color.Transparent, 0.58f to Color.Transparent,
1f to MembySurface.copy(alpha = 0.24f), 1f to MembySurface.copy(alpha = 0.20f),
), ),
), ),
) )
@@ -126,16 +121,16 @@ private fun MetadataHeroArtwork(
model = request, model = request,
contentDescription = null, contentDescription = null,
contentScale = ContentScale.Crop, contentScale = ContentScale.Crop,
alignment = Alignment.Center, alignment = Alignment.CenterEnd,
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
) )
Box( Box(
Modifier.fillMaxSize().background( Modifier.fillMaxSize().background(
Brush.horizontalGradient( Brush.horizontalGradient(
0f to MembySurface, 0f to MembySurface.copy(alpha = 0.92f),
0.16f to MembySurface.copy(alpha = 0.96f), 0.18f to MembySurface.copy(alpha = 0.76f),
0.42f to MembySurface.copy(alpha = 0.70f), 0.42f to MembySurface.copy(alpha = 0.42f),
0.76f to MembySurface.copy(alpha = 0.24f), 0.70f to MembySurface.copy(alpha = 0.12f),
1f to Color.Transparent, 1f to Color.Transparent,
), ),
), ),
@@ -143,9 +138,9 @@ private fun MetadataHeroArtwork(
Box( Box(
Modifier.fillMaxSize().background( Modifier.fillMaxSize().background(
Brush.verticalGradient( Brush.verticalGradient(
0f to MembySurface.copy(alpha = 0.28f), 0f to MembySurface.copy(alpha = 0.16f),
0.58f to Color.Transparent, 0.56f to Color.Transparent,
1f to MembySurface.copy(alpha = 0.48f), 1f to MembySurface.copy(alpha = 0.28f),
), ),
), ),
) )
@@ -154,4 +149,4 @@ private fun MetadataHeroArtwork(
} }
private const val MetadataHeroArtworkCrossfadeMs = 180 private const val MetadataHeroArtworkCrossfadeMs = 180
private const val MetadataHeroArtworkWidthFraction = 0.58f private const val MetadataHeroArtworkWidthFraction = 0.68f
@@ -120,6 +120,7 @@ import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlin.time.Duration.Companion.milliseconds import kotlin.time.Duration.Companion.milliseconds
@Composable
fun ContinueWatchingCard( fun ContinueWatchingCard(
item: BaseItem, item: BaseItem,
availableWidth: Dp, availableWidth: Dp,
@@ -120,6 +120,7 @@ import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlin.time.Duration.Companion.milliseconds import kotlin.time.Duration.Companion.milliseconds
@Composable
fun LandscapeCard( fun LandscapeCard(
item: BaseItem, item: BaseItem,
availableWidth: Dp, availableWidth: Dp,
@@ -150,7 +150,7 @@ fun MediaBadge(label: String, modifier: Modifier = Modifier) {
} }
@Composable @Composable
private fun ScheduleStatusBadge(status: String, label: String, modifier: Modifier = Modifier) { internal fun ScheduleStatusBadge(status: String, label: String, modifier: Modifier = Modifier) {
val color = when (status) { val color = when (status) {
"available" -> EmbyGreen "available" -> EmbyGreen
"downloading" -> Color(0xFF5DA9FF) "downloading" -> Color(0xFF5DA9FF)
@@ -197,7 +197,7 @@ fun PosterGridCard(
@Composable @Composable
private fun MediaCard( internal fun MediaCard(
item: BaseItem, item: BaseItem,
width: Dp, width: Dp,
aspectRatio: Float, aspectRatio: Float,
@@ -135,6 +135,8 @@ private val RailSurface: Color get() = MembySurface.copy(alpha = 0.95f)
private val MutedText: Color get() = MembyMutedText private val MutedText: Color get() = MembyMutedText
private val QuietText: Color get() = MembyQuietText private val QuietText: Color get() = MembyQuietText
private val BACKDROP_SETTLE_DELAY = 240.milliseconds private val BACKDROP_SETTLE_DELAY = 240.milliseconds
private val MetadataHeroLogoMaxWidth = 420.dp
private val MetadataHeroLogoMaxHeight = 92.dp
@Composable @Composable
fun BackdropLayer(item: BaseItem?, modifier: Modifier = Modifier) { fun BackdropLayer(item: BaseItem?, modifier: Modifier = Modifier) {
@@ -227,8 +229,8 @@ fun MediaMetadataPanel(
} }
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.78f else 0.68f)
.coerceIn(280.dp, 680.dp) .coerceIn(280.dp, 620.dp)
.coerceAtMost(availableWidth) .coerceAtMost(availableWidth)
@Composable @Composable
@@ -268,25 +270,65 @@ private fun MetadataContent(
ScheduleMetadataContent(item, sectionLabel, contentWidth, compact) ScheduleMetadataContent(item, sectionLabel, contentWidth, compact)
return return
} }
val repository = ServiceLocator.repository
val logoUrl = remember(
item.id,
item.imageTags,
item.parentLogoItemId,
item.parentLogoImageTag,
repository.showTitleLogo,
) {
if (repository.showTitleLogo) repository.logoUrl(item) else null
}
val logo = logoUrl.takeIf { !useTextTitleForLogo(it) }
Column( Column(
modifier = Modifier.width(contentWidth).fillMaxHeight(), modifier = Modifier.width(contentWidth).fillMaxHeight(),
verticalArrangement = Arrangement.spacedBy(if (compact) 5.dp else 8.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( Box(
item.seriesName?.takeIf { item.isEpisode } ?: item.name, modifier = Modifier
color = Color.White, .fillMaxWidth()
fontSize = if (compact) 25.sp else 29.sp, .then(
lineHeight = if (compact) 28.sp else 32.sp, if (logoUrl != null) {
fontWeight = FontWeight.SemiBold, Modifier.height(MetadataHeroLogoMaxHeight)
maxLines = 1, } else {
overflow = TextOverflow.Ellipsis, Modifier.heightIn(max = MetadataHeroLogoMaxHeight)
) },
),
contentAlignment = Alignment.BottomStart,
) {
if (logo != null) {
TitleLogoImage(
logoUrl = logo,
contentDescription = item.seriesName?.takeIf { item.isEpisode } ?: item.name,
alignment = Alignment.BottomStart,
modifier = Modifier
.width(MetadataHeroLogoMaxWidth)
.height(MetadataHeroLogoMaxHeight),
)
} else {
Text(
item.seriesName?.takeIf { item.isEpisode } ?: item.name,
color = Color.White,
fontSize = if (compact) 25.sp else 29.sp,
lineHeight = if (compact) 28.sp else 32.sp,
fontWeight = FontWeight.SemiBold,
maxLines = 2,
overflow = TextOverflow.Ellipsis,
)
}
}
if (item.isEpisode && item.seriesName != null) { if (item.isEpisode && item.seriesName != null) {
Text(item.name, color = MutedText, fontSize = 14.sp, maxLines = 1, overflow = TextOverflow.Ellipsis) Text(
listOfNotNull(item.episodeCode, item.name.takeIf(String::isNotBlank)).joinToString(FactSeparator),
color = MutedText,
fontSize = 14.sp,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
} }
val facts = buildList { val facts = buildList {
item.episodeCode?.let(::add)
item.productionYear?.let { add(it.toString()) } item.productionYear?.let { add(it.toString()) }
item.runtimeMinutes?.let { add(formatRuntime(it)) } item.runtimeMinutes?.let { add(formatRuntime(it)) }
item.officialRating?.takeIf(String::isNotBlank)?.let(::add) item.officialRating?.takeIf(String::isNotBlank)?.let(::add)
@@ -138,6 +138,8 @@ private data class HomeRowVisual(
val icon: ImageVector, val icon: ImageVector,
) )
private val QuietText: Color get() = MembyQuietText
private fun homeRowVisual(row: HomeBrowseRow): HomeRowVisual = when { private fun homeRowVisual(row: HomeBrowseRow): HomeRowVisual = when {
row.id == "continue" -> HomeRowVisual( row.id == "continue" -> HomeRowVisual(
MembyIcon.PlayCircle.mark, MembyIcon.PlayCircle.mark,
@@ -471,41 +473,6 @@ private fun GalleryJumpButton(
private enum class MediaCardFormat { PORTRAIT, LANDSCAPE } private enum class MediaCardFormat { PORTRAIT, LANDSCAPE }
private val MediaRowHorizontalInset = HomeContentHorizontalInset
private val MediaRowCardSpacing = 16.dp
/**
* Sizes a shelf to an integer number of complete cards.
*
* 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,
preferredCardsAcross: Int,
minWidth: Dp,
maxWidth: Dp,
horizontalInset: Dp = MediaRowHorizontalInset,
spacing: Dp = MediaRowCardSpacing,
): Dp {
val usableWidth = (availableWidth - horizontalInset * 2).coerceAtLeast(1.dp)
val cardsThatFitMinimum =
((usableWidth + spacing) / (minWidth + spacing)).toInt().coerceAtLeast(1)
var cardCount = preferredCardsAcross.coerceIn(1, cardsThatFitMinimum)
// Very wide viewports should gain another complete card instead of a large dead area.
while (cardCount < cardsThatFitMinimum) {
val candidate = (usableWidth - spacing * (cardCount - 1)) / cardCount
if (candidate <= maxWidth) break
cardCount += 1
}
return ((usableWidth - spacing * (cardCount - 1)) / cardCount)
.coerceAtMost(maxWidth)
.coerceAtLeast(1.dp)
}
private fun cardFormat( private fun cardFormat(
kind: MediaRowKind, kind: MediaRowKind,
item: BaseItem, item: BaseItem,
@@ -10,8 +10,8 @@ import org.junit.Test
class ResponsiveRowSizingTest { class ResponsiveRowSizingTest {
@Test @Test
fun metadataPanelKeepsReadableLineLengthsAcrossTvWidths() { fun metadataPanelKeepsReadableLineLengthsAcrossTvWidths() {
assertEquals(537.6f, metadataPanelContentWidth(640.dp, compact = true).value, 0.01f) assertEquals(499.2f, metadataPanelContentWidth(640.dp, compact = true).value, 0.01f)
assertEquals(680f, metadataPanelContentWidth(1920.dp, compact = false).value, 0.01f) assertEquals(620f, metadataPanelContentWidth(1920.dp, compact = false).value, 0.01f)
} }
@Test @Test
@@ -50,7 +50,7 @@ class WatchedVisibilityScreenshotTest {
horizontalArrangement = Arrangement.spacedBy(18.dp), horizontalArrangement = Arrangement.spacedBy(18.dp),
) { ) {
visible.items.forEach { item -> visible.items.forEach { item ->
PortraitMediaCard( PosterCard(
item = item, item = item,
availableWidth = 820.dp, availableWidth = 820.dp,
showSecondaryMetadata = true, showSecondaryMetadata = true,
@@ -102,7 +102,6 @@ class ViewerPickerScreenshotTest {
viewers = household, viewers = household,
canAddViewer = true, canAddViewer = true,
activeViewerId = "v1", activeViewerId = "v1",
activeViewerName = "Alessandra",
) )
} }
} }