diff --git a/CHANGELOG.md b/CHANGELOG.md index d030cb6..2b5fc85 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 - Improved: Loading and performance of the continue watching row. diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 9a38c75..c29767a 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -64,7 +64,7 @@ val projectNoticeText = // 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. -val defaultVersionName = "0.2.91" +val defaultVersionName = "0.2.92" val membyVersionName: String = (project.findProperty("memby.versionName") as String?) ?.trim() diff --git a/app/src/main/java/com/ponzischeme89/memby/ui/MetadataHero.kt b/app/src/main/java/com/ponzischeme89/memby/ui/MetadataHero.kt index bf3f9e8..572bc8b 100644 --- a/app/src/main/java/com/ponzischeme89/memby/ui/MetadataHero.kt +++ b/app/src/main/java/com/ponzischeme89/memby/ui/MetadataHero.kt @@ -40,13 +40,7 @@ internal fun MetadataHero( Box( modifier = modifier .clipToBounds() - .background( - Brush.verticalGradient( - 0f to MembySurface.copy(alpha = 0.74f), - 0.70f to MembySurface.copy(alpha = 0.52f), - 1f to MembySurface.copy(alpha = 0.18f), - ), - ), + .background(MembySurface), ) { MetadataHeroArtwork( item = item, @@ -55,20 +49,21 @@ internal fun MetadataHero( Box( Modifier.fillMaxSize().background( Brush.horizontalGradient( - 0f to MembySurface.copy(alpha = 0.94f), - 0.34f to MembySurface.copy(alpha = 0.90f), - 0.60f to MembySurface.copy(alpha = 0.74f), - 0.82f to MembySurface.copy(alpha = 0.38f), - 1f to MembySurface.copy(alpha = 0.18f), + 0f to MembySurface, + 0.30f to MembySurface, + 0.48f to MembySurface.copy(alpha = 0.80f), + 0.62f to MembySurface.copy(alpha = 0.40f), + 0.78f to MembySurface.copy(alpha = 0.10f), + 1f to Color.Transparent, ), ), ) Box( Modifier.fillMaxSize().background( Brush.verticalGradient( - 0f to MembySurface.copy(alpha = 0.28f), - 0.74f to Color.Transparent, - 1f to MembySurface.copy(alpha = 0.24f), + 0f to MembySurface.copy(alpha = 0.18f), + 0.58f to Color.Transparent, + 1f to MembySurface.copy(alpha = 0.20f), ), ), ) @@ -126,16 +121,16 @@ private fun MetadataHeroArtwork( model = request, contentDescription = null, contentScale = ContentScale.Crop, - alignment = Alignment.Center, + alignment = Alignment.CenterEnd, modifier = Modifier.fillMaxSize(), ) Box( Modifier.fillMaxSize().background( Brush.horizontalGradient( - 0f to MembySurface, - 0.16f to MembySurface.copy(alpha = 0.96f), - 0.42f to MembySurface.copy(alpha = 0.70f), - 0.76f to MembySurface.copy(alpha = 0.24f), + 0f to MembySurface.copy(alpha = 0.92f), + 0.18f to MembySurface.copy(alpha = 0.76f), + 0.42f to MembySurface.copy(alpha = 0.42f), + 0.70f to MembySurface.copy(alpha = 0.12f), 1f to Color.Transparent, ), ), @@ -143,9 +138,9 @@ private fun MetadataHeroArtwork( Box( Modifier.fillMaxSize().background( Brush.verticalGradient( - 0f to MembySurface.copy(alpha = 0.28f), - 0.58f to Color.Transparent, - 1f to MembySurface.copy(alpha = 0.48f), + 0f to MembySurface.copy(alpha = 0.16f), + 0.56f to Color.Transparent, + 1f to MembySurface.copy(alpha = 0.28f), ), ), ) @@ -154,4 +149,4 @@ private fun MetadataHeroArtwork( } private const val MetadataHeroArtworkCrossfadeMs = 180 -private const val MetadataHeroArtworkWidthFraction = 0.58f +private const val MetadataHeroArtworkWidthFraction = 0.68f diff --git a/app/src/main/java/com/ponzischeme89/memby/ui/components/media/ContinueWatchingCard.kt b/app/src/main/java/com/ponzischeme89/memby/ui/components/media/ContinueWatchingCard.kt index bfda9b8..8ed8e09 100644 --- a/app/src/main/java/com/ponzischeme89/memby/ui/components/media/ContinueWatchingCard.kt +++ b/app/src/main/java/com/ponzischeme89/memby/ui/components/media/ContinueWatchingCard.kt @@ -120,6 +120,7 @@ import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlin.time.Duration.Companion.milliseconds +@Composable fun ContinueWatchingCard( item: BaseItem, availableWidth: Dp, diff --git a/app/src/main/java/com/ponzischeme89/memby/ui/components/media/LandscapeCard.kt b/app/src/main/java/com/ponzischeme89/memby/ui/components/media/LandscapeCard.kt index 5e005a1..2668209 100644 --- a/app/src/main/java/com/ponzischeme89/memby/ui/components/media/LandscapeCard.kt +++ b/app/src/main/java/com/ponzischeme89/memby/ui/components/media/LandscapeCard.kt @@ -120,6 +120,7 @@ import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlin.time.Duration.Companion.milliseconds +@Composable fun LandscapeCard( item: BaseItem, availableWidth: Dp, diff --git a/app/src/main/java/com/ponzischeme89/memby/ui/components/media/MediaBadges.kt b/app/src/main/java/com/ponzischeme89/memby/ui/components/media/MediaBadges.kt index 187888d..bc9d633 100644 --- a/app/src/main/java/com/ponzischeme89/memby/ui/components/media/MediaBadges.kt +++ b/app/src/main/java/com/ponzischeme89/memby/ui/components/media/MediaBadges.kt @@ -150,7 +150,7 @@ fun MediaBadge(label: String, modifier: Modifier = Modifier) { } @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) { "available" -> EmbyGreen "downloading" -> Color(0xFF5DA9FF) diff --git a/app/src/main/java/com/ponzischeme89/memby/ui/components/media/PosterCard.kt b/app/src/main/java/com/ponzischeme89/memby/ui/components/media/PosterCard.kt index a87145b..41847f8 100644 --- a/app/src/main/java/com/ponzischeme89/memby/ui/components/media/PosterCard.kt +++ b/app/src/main/java/com/ponzischeme89/memby/ui/components/media/PosterCard.kt @@ -197,7 +197,7 @@ fun PosterGridCard( @Composable -private fun MediaCard( +internal fun MediaCard( item: BaseItem, width: Dp, aspectRatio: Float, diff --git a/app/src/main/java/com/ponzischeme89/memby/ui/home/HomeMetadataPanel.kt b/app/src/main/java/com/ponzischeme89/memby/ui/home/HomeMetadataPanel.kt index 231e3a3..bdcc052 100644 --- a/app/src/main/java/com/ponzischeme89/memby/ui/home/HomeMetadataPanel.kt +++ b/app/src/main/java/com/ponzischeme89/memby/ui/home/HomeMetadataPanel.kt @@ -135,6 +135,8 @@ private val RailSurface: Color get() = MembySurface.copy(alpha = 0.95f) private val MutedText: Color get() = MembyMutedText private val QuietText: Color get() = MembyQuietText private val BACKDROP_SETTLE_DELAY = 240.milliseconds +private val MetadataHeroLogoMaxWidth = 420.dp +private val MetadataHeroLogoMaxHeight = 92.dp @Composable fun BackdropLayer(item: BaseItem?, modifier: Modifier = Modifier) { @@ -227,8 +229,8 @@ fun MediaMetadataPanel( } internal fun metadataPanelContentWidth(availableWidth: Dp, compact: Boolean): Dp = - (availableWidth * if (compact) 0.84f else 0.76f) - .coerceIn(280.dp, 680.dp) + (availableWidth * if (compact) 0.78f else 0.68f) + .coerceIn(280.dp, 620.dp) .coerceAtMost(availableWidth) @Composable @@ -268,25 +270,65 @@ private fun MetadataContent( ScheduleMetadataContent(item, sectionLabel, contentWidth, compact) 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( modifier = Modifier.width(contentWidth).fillMaxHeight(), 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( - 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 = 1, - overflow = TextOverflow.Ellipsis, - ) + Box( + modifier = Modifier + .fillMaxWidth() + .then( + if (logoUrl != null) { + Modifier.height(MetadataHeroLogoMaxHeight) + } else { + 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) { - 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 { - item.episodeCode?.let(::add) item.productionYear?.let { add(it.toString()) } item.runtimeMinutes?.let { add(formatRuntime(it)) } item.officialRating?.takeIf(String::isNotBlank)?.let(::add) diff --git a/app/src/main/java/com/ponzischeme89/memby/ui/home/HomeRow.kt b/app/src/main/java/com/ponzischeme89/memby/ui/home/HomeRow.kt index edcdbdb..554ddc3 100644 --- a/app/src/main/java/com/ponzischeme89/memby/ui/home/HomeRow.kt +++ b/app/src/main/java/com/ponzischeme89/memby/ui/home/HomeRow.kt @@ -138,6 +138,8 @@ private data class HomeRowVisual( val icon: ImageVector, ) +private val QuietText: Color get() = MembyQuietText + private fun homeRowVisual(row: HomeBrowseRow): HomeRowVisual = when { row.id == "continue" -> HomeRowVisual( MembyIcon.PlayCircle.mark, @@ -471,41 +473,6 @@ private fun GalleryJumpButton( 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( kind: MediaRowKind, item: BaseItem, diff --git a/app/src/test/java/com/ponzischeme89/memby/ui/ResponsiveRowSizingTest.kt b/app/src/test/java/com/ponzischeme89/memby/ui/ResponsiveRowSizingTest.kt index 67eaeec..83ee80e 100644 --- a/app/src/test/java/com/ponzischeme89/memby/ui/ResponsiveRowSizingTest.kt +++ b/app/src/test/java/com/ponzischeme89/memby/ui/ResponsiveRowSizingTest.kt @@ -10,8 +10,8 @@ import org.junit.Test class ResponsiveRowSizingTest { @Test fun metadataPanelKeepsReadableLineLengthsAcrossTvWidths() { - assertEquals(537.6f, metadataPanelContentWidth(640.dp, compact = true).value, 0.01f) - assertEquals(680f, metadataPanelContentWidth(1920.dp, compact = false).value, 0.01f) + assertEquals(499.2f, metadataPanelContentWidth(640.dp, compact = true).value, 0.01f) + assertEquals(620f, metadataPanelContentWidth(1920.dp, compact = false).value, 0.01f) } @Test diff --git a/app/src/test/java/com/ponzischeme89/memby/ui/WatchedVisibilityScreenshotTest.kt b/app/src/test/java/com/ponzischeme89/memby/ui/WatchedVisibilityScreenshotTest.kt index 5890628..1afafdb 100644 --- a/app/src/test/java/com/ponzischeme89/memby/ui/WatchedVisibilityScreenshotTest.kt +++ b/app/src/test/java/com/ponzischeme89/memby/ui/WatchedVisibilityScreenshotTest.kt @@ -50,7 +50,7 @@ class WatchedVisibilityScreenshotTest { horizontalArrangement = Arrangement.spacedBy(18.dp), ) { visible.items.forEach { item -> - PortraitMediaCard( + PosterCard( item = item, availableWidth = 820.dp, showSecondaryMetadata = true, diff --git a/app/src/test/java/com/ponzischeme89/memby/ui/viewers/ViewerPickerScreenshotTest.kt b/app/src/test/java/com/ponzischeme89/memby/ui/viewers/ViewerPickerScreenshotTest.kt index 4f902b7..20ffb5c 100644 --- a/app/src/test/java/com/ponzischeme89/memby/ui/viewers/ViewerPickerScreenshotTest.kt +++ b/app/src/test/java/com/ponzischeme89/memby/ui/viewers/ViewerPickerScreenshotTest.kt @@ -102,7 +102,6 @@ class ViewerPickerScreenshotTest { viewers = household, canAddViewer = true, activeViewerId = "v1", - activeViewerName = "Alessandra", ) } }