This commit is contained in:
ponzischeme89
2026-08-21 21:04:57 +12:00
parent 9c7dec32a8
commit 02247a6287
10 changed files with 156 additions and 156 deletions
+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
// source tree. Local builds keep using the checked-in default.
val defaultVersionName = "0.2.93"
val defaultVersionName = "0.2.94"
val membyVersionName: String =
(project.findProperty("memby.versionName") as String?)
?.trim()
@@ -242,6 +242,8 @@ internal object SettingsMigrationLogic {
booleanPreferencesKey("show_home_card_metadata")
] ?: true,
showRatingsStrip = preferences[booleanPreferencesKey("show_ratings_strip")] ?: true,
metadataHeroContentOrder = preferences[stringPreferencesKey("metadata_hero_content_order")]
?: UserPreferences.DEFAULT_METADATA_HERO_CONTENT_ORDER.joinToString("\n"),
hideWatchedMovies = preferences[booleanPreferencesKey("hide_watched_movies")] ?: false,
homeRowOrder = preferences[stringPreferencesKey("home_row_order")].orEmpty(),
homePinnedRows = preferences[stringPreferencesKey("home_pinned_rows")].orEmpty(),
@@ -363,6 +365,8 @@ data class Settings(
val showHomeCardMetadata: Boolean = true,
/** Show third-party ratings on browse and detail surfaces. Profile-specific. */
val showRatingsStrip: Boolean = true,
/** Newline-separated, operator-controlled content order for the focused metadata hero. */
val metadataHeroContentOrder: String = UserPreferences.DEFAULT_METADATA_HERO_CONTENT_ORDER.joinToString("\n"),
/** Hide fully watched movies from browse rows and hero selections. */
val hideWatchedMovies: Boolean = false,
/** Newline-separated server row ids. These are profile-specific home customisations. */
@@ -511,6 +515,7 @@ data class EmbyProfile(
val homeArtworkStyle: String = Settings.DEFAULT_HOME_ARTWORK_STYLE,
val showHomeCardMetadata: Boolean = true,
val showRatingsStrip: Boolean = true,
val metadataHeroContentOrder: String = UserPreferences.DEFAULT_METADATA_HERO_CONTENT_ORDER.joinToString("\n"),
val hideWatchedMovies: Boolean = false,
val homeRowOrder: String = "",
val homePinnedRows: String = "",
@@ -590,6 +595,7 @@ class SettingsStore(private val context: Context) {
val HOME_ARTWORK_STYLE = stringPreferencesKey("home_artwork_style")
val SHOW_HOME_CARD_METADATA = booleanPreferencesKey("show_home_card_metadata")
val SHOW_RATINGS_STRIP = booleanPreferencesKey("show_ratings_strip")
val METADATA_HERO_CONTENT_ORDER = stringPreferencesKey("metadata_hero_content_order")
val HIDE_WATCHED_MOVIES = booleanPreferencesKey("hide_watched_movies")
val HOME_ROW_ORDER = stringPreferencesKey("home_row_order")
val HOME_PINNED_ROWS = stringPreferencesKey("home_pinned_rows")
@@ -862,6 +868,7 @@ class SettingsStore(private val context: Context) {
store[Keys.HOME_ARTWORK_STYLE] = preferences.homeArtworkStyle
store[Keys.SHOW_HOME_CARD_METADATA] = preferences.showHomeCardMetadata
store[Keys.SHOW_RATINGS_STRIP] = preferences.showRatingsStrip
store[Keys.METADATA_HERO_CONTENT_ORDER] = preferences.metadataHeroContentOrder.joinToString("\n")
store[Keys.HIDE_WATCHED_MOVIES] = preferences.hideWatchedMovies
store[Keys.SHOW_TITLE_LOGO] = preferences.showTitleLogo
store[Keys.WELCOME_QUOTE_STYLE] = preferences.welcomeQuoteStyle
@@ -888,6 +895,7 @@ class SettingsStore(private val context: Context) {
homeArtworkStyle = preferences.homeArtworkStyle,
showHomeCardMetadata = preferences.showHomeCardMetadata,
showRatingsStrip = preferences.showRatingsStrip,
metadataHeroContentOrder = preferences.metadataHeroContentOrder.joinToString("\n"),
hideWatchedMovies = preferences.hideWatchedMovies,
showTitleLogo = preferences.showTitleLogo,
welcomeQuoteStyle = preferences.welcomeQuoteStyle,
@@ -1348,6 +1356,8 @@ class SettingsStore(private val context: Context) {
?: Settings.DEFAULT_HOME_ARTWORK_STYLE,
showHomeCardMetadata = previous?.showHomeCardMetadata ?: true,
showRatingsStrip = previous?.showRatingsStrip ?: true,
metadataHeroContentOrder = previous?.metadataHeroContentOrder
?: UserPreferences.DEFAULT_METADATA_HERO_CONTENT_ORDER.joinToString("\n"),
hideWatchedMovies = previous?.hideWatchedMovies ?: false,
homeRowOrder = previous?.homeRowOrder.orEmpty(),
homePinnedRows = previous?.homePinnedRows.orEmpty(),
@@ -1526,6 +1536,7 @@ class SettingsStore(private val context: Context) {
preferences[Keys.HOME_ARTWORK_STYLE] = profile.homeArtworkStyle
preferences[Keys.SHOW_HOME_CARD_METADATA] = profile.showHomeCardMetadata
preferences[Keys.SHOW_RATINGS_STRIP] = profile.showRatingsStrip
preferences[Keys.METADATA_HERO_CONTENT_ORDER] = profile.metadataHeroContentOrder
preferences[Keys.HIDE_WATCHED_MOVIES] = profile.hideWatchedMovies
preferences[Keys.HOME_ROW_ORDER] = profile.homeRowOrder
preferences[Keys.HOME_PINNED_ROWS] = profile.homePinnedRows
@@ -1569,6 +1580,8 @@ class SettingsStore(private val context: Context) {
?: Settings.DEFAULT_HOME_ARTWORK_STYLE,
showHomeCardMetadata = preferences[Keys.SHOW_HOME_CARD_METADATA] ?: true,
showRatingsStrip = preferences[Keys.SHOW_RATINGS_STRIP] ?: true,
metadataHeroContentOrder = preferences[Keys.METADATA_HERO_CONTENT_ORDER]
?: UserPreferences.DEFAULT_METADATA_HERO_CONTENT_ORDER.joinToString("\n"),
hideWatchedMovies = preferences[Keys.HIDE_WATCHED_MOVIES] ?: false,
homeRowOrder = preferences[Keys.HOME_ROW_ORDER].orEmpty(),
homePinnedRows = preferences[Keys.HOME_PINNED_ROWS].orEmpty(),
@@ -1637,6 +1650,8 @@ class SettingsStore(private val context: Context) {
?: Settings.DEFAULT_HOME_ARTWORK_STYLE,
showHomeCardMetadata = preferences[Keys.SHOW_HOME_CARD_METADATA] ?: true,
showRatingsStrip = preferences[Keys.SHOW_RATINGS_STRIP] ?: true,
metadataHeroContentOrder = preferences[Keys.METADATA_HERO_CONTENT_ORDER]
?: UserPreferences.DEFAULT_METADATA_HERO_CONTENT_ORDER.joinToString("\n"),
hideWatchedMovies = preferences[Keys.HIDE_WATCHED_MOVIES] ?: false,
homeRowOrder = preferences[Keys.HOME_ROW_ORDER].orEmpty(),
homePinnedRows = preferences[Keys.HOME_PINNED_ROWS].orEmpty(),
@@ -36,6 +36,8 @@ data class UserPreferences(
val homeArtworkStyle: String = Settings.DEFAULT_HOME_ARTWORK_STYLE,
val showHomeCardMetadata: Boolean = true,
val showRatingsStrip: Boolean = true,
/** Operator-controlled order of the focused metadata hero's content blocks. */
val metadataHeroContentOrder: List<String> = DEFAULT_METADATA_HERO_CONTENT_ORDER,
val hideWatchedMovies: Boolean = false,
val showTitleLogo: Boolean = true,
val welcomeQuoteStyle: String = Settings.DEFAULT_WELCOME_QUOTE_STYLE,
@@ -73,6 +75,7 @@ data class UserPreferences(
) {
companion object {
val DEFAULT_SECTIONS: List<String> = Settings.DEFAULT_HOME_SECTIONS.split(",")
val DEFAULT_METADATA_HERO_CONTENT_ORDER = listOf("title", "ratings", "facts", "genres", "summary")
}
}
@@ -90,6 +93,7 @@ fun Settings.toUserPreferences(): UserPreferences = UserPreferences(
homeArtworkStyle = homeArtworkStyle,
showHomeCardMetadata = showHomeCardMetadata,
showRatingsStrip = showRatingsStrip,
metadataHeroContentOrder = metadataHeroContentOrder.decodeLineList(),
hideWatchedMovies = hideWatchedMovies,
showTitleLogo = showTitleLogo,
welcomeQuoteStyle = welcomeQuoteStyle,
@@ -134,6 +138,9 @@ fun decodeUserPreferences(
homeArtworkStyle = json.string("homeArtworkStyle", fallback.homeArtworkStyle),
showHomeCardMetadata = json.boolean("showHomeCardMetadata", fallback.showHomeCardMetadata),
showRatingsStrip = json.boolean("showRatingsStrip", fallback.showRatingsStrip),
metadataHeroContentOrder = json.stringList(
"metadataHeroContentOrder", fallback.metadataHeroContentOrder,
),
hideWatchedMovies = json.boolean("hideWatchedMovies", fallback.hideWatchedMovies),
showTitleLogo = json.boolean("showTitleLogo", fallback.showTitleLogo),
welcomeQuoteStyle = json.string("welcomeQuoteStyle", fallback.welcomeQuoteStyle),
@@ -169,6 +176,7 @@ fun UserPreferences.encode(): JsonObject = buildJsonObject {
put("homeArtworkStyle", homeArtworkStyle)
put("showHomeCardMetadata", showHomeCardMetadata)
put("showRatingsStrip", showRatingsStrip)
putJsonArray("metadataHeroContentOrder") { metadataHeroContentOrder.forEach { add(JsonPrimitive(it)) } }
put("hideWatchedMovies", hideWatchedMovies)
put("showTitleLogo", showTitleLogo)
put("welcomeQuoteStyle", welcomeQuoteStyle)
@@ -2,6 +2,7 @@ package com.ponzischeme89.memby.ui
import com.ponzischeme89.memby.ui.theme.MembyIcon
import com.ponzischeme89.memby.ui.theme.mark
import com.ponzischeme89.memby.data.decodeLineList
import android.os.Build
import android.text.format.DateFormat
import androidx.compose.foundation.background
@@ -283,6 +284,7 @@ internal fun FocusedHomeBackdrop(homeViewModel: HomeViewModel) {
@Composable
internal fun FocusedHomeMetadata(
homeViewModel: HomeViewModel,
metadataHeroContentOrder: List<String>,
modifier: Modifier = Modifier,
) {
val focusedItem by homeViewModel.focusedItem.collectAsStateWithLifecycle()
@@ -293,6 +295,7 @@ internal fun FocusedHomeMetadata(
MetadataHero(
item = focusedItem,
loading = homeContent.loading.isNotEmpty(),
contentOrder = metadataHeroContentOrder,
modifier = modifier,
)
}
@@ -1376,6 +1376,7 @@ internal fun HomeScreen(
} else {
FocusedHomeMetadata(
homeViewModel = homeViewModel,
metadataHeroContentOrder = settings.metadataHeroContentOrder.decodeLineList(),
modifier = Modifier
.height(metadataHeight)
// LazyColumn is drawn later as a sibling. Keep the hero
@@ -34,6 +34,7 @@ import com.ponzischeme89.memby.ui.theme.MembySurface
internal fun MetadataHero(
item: BaseItem?,
loading: Boolean,
contentOrder: List<String> = emptyList(),
modifier: Modifier = Modifier,
) {
Box(
@@ -69,6 +70,7 @@ internal fun MetadataHero(
MediaMetadataPanel(
item = item,
loading = loading,
contentOrder = contentOrder,
modifier = Modifier
.fillMaxSize()
.padding(
@@ -2,122 +2,55 @@ package com.ponzischeme89.memby.ui
import com.ponzischeme89.memby.ui.theme.MembyIcon
import com.ponzischeme89.memby.ui.theme.mark
import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.core.animateDpAsState
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.tween
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.focusGroup
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.BoxScope
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.wrapContentWidth
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.clipToBounds
import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusProperties
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.layout.layout
import androidx.compose.ui.input.key.onPreviewKeyEvent
import androidx.compose.ui.input.key.Key
import androidx.compose.ui.input.key.KeyEventType
import androidx.compose.ui.input.key.key
import androidx.compose.ui.input.key.type
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.onLongClick
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.zIndex
import androidx.tv.material3.Icon
import androidx.tv.material3.Text
import coil.compose.AsyncImage
import coil.request.ImageRequest
import com.ponzischeme89.memby.ServiceLocator
import com.ponzischeme89.memby.R
import com.ponzischeme89.memby.BuildConfig
import com.ponzischeme89.memby.data.EmbyProfile
import com.ponzischeme89.memby.data.model.BaseItem
import com.ponzischeme89.memby.data.model.MembyViewer
import com.ponzischeme89.memby.ui.alerts.alertBadgeLabel
import com.ponzischeme89.memby.ui.detail.UHD_MIN_WIDTH
import com.ponzischeme89.memby.ui.detail.channelLabel
import com.ponzischeme89.memby.ui.detail.dynamicRangeLabel
import com.ponzischeme89.memby.ui.detail.formatRuntime
import com.ponzischeme89.memby.ui.theme.FactSeparator
import com.ponzischeme89.memby.ui.theme.MembyAccent
import com.ponzischeme89.memby.ui.theme.MembyAccentInk
import com.ponzischeme89.memby.ui.theme.MembyCardCorner
import com.ponzischeme89.memby.ui.theme.MembyChipCorner
import com.ponzischeme89.memby.ui.theme.MembyControlSurface
import com.ponzischeme89.memby.ui.theme.MembyControlSurfaceRaised
import com.ponzischeme89.memby.ui.theme.MembyMutedText
import com.ponzischeme89.memby.ui.theme.MembyOnSurface
import com.ponzischeme89.memby.ui.theme.MembyPanelCorner
import com.ponzischeme89.memby.ui.theme.MembyQuietText
import com.ponzischeme89.memby.ui.theme.MembySurface
import com.ponzischeme89.memby.ui.theme.MembySurfaceRaised
import com.ponzischeme89.memby.ui.theme.ValueSeparator
import com.ponzischeme89.memby.data.remoteconfig.BundledRemoteConfig
import com.ponzischeme89.memby.data.remoteconfig.NavigationLabels
import com.ponzischeme89.memby.data.remoteconfig.NavigationRemoteConfig
import java.util.Locale
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlin.time.Duration.Companion.milliseconds
// The launcher's names for the shared tokens. Both surfaces read from one palette now
@@ -196,6 +129,7 @@ fun BackdropLayer(item: BaseItem?, modifier: Modifier = Modifier) {
fun MediaMetadataPanel(
item: BaseItem?,
loading: Boolean,
contentOrder: List<String>,
modifier: Modifier = Modifier,
) {
BoxWithConstraints(modifier) {
@@ -219,7 +153,7 @@ fun MediaMetadataPanel(
Text("Choose something to watch.", color = MutedText, fontSize = 14.sp)
}
}
else -> MetadataContent(item, contentWidth, compactLayout)
else -> MetadataContent(item, contentWidth, compactLayout, contentOrder)
}
}
}
@@ -262,6 +196,7 @@ private fun MetadataContent(
item: BaseItem,
contentWidth: Dp,
compact: Boolean,
contentOrder: List<String>,
) {
if (item.isSchedule) {
ScheduleMetadataContent(item, contentWidth, compact)
@@ -278,19 +213,74 @@ private fun MetadataContent(
if (repository.showTitleLogo) repository.logoUrl(item) else null
}
val logo = logoUrl.takeIf { !useTextTitleForLogo(it) }
val facts = buildList {
item.productionYear?.let { add(it.toString()) }
item.runtimeMinutes?.let { add(formatRuntime(it)) }
item.officialRating?.takeIf(String::isNotBlank)?.let(::add)
}
val badges = buildList {
airingBadgeLabel(item)?.let(::add)
addAll(mediaBadges(item))
}
val genres = item.genres.take(2).joinToString(ValueSeparator)
Column(
modifier = Modifier.width(contentWidth).fillMaxHeight(),
verticalArrangement = Arrangement.spacedBy(if (compact) 5.dp else 8.dp),
) {
metadataHeroContentOrder(contentOrder).forEach { section ->
when (section) {
MetadataHeroSection.Title -> MetadataHeroTitle(item, logoUrl, logo, compact)
MetadataHeroSection.Ratings -> ItemRatingsStrip(
item = item, load = true, compact = true, modifier = Modifier.fillMaxWidth(),
)
MetadataHeroSection.Facts -> {
// Ratings identify their provider in the strip; this line is deliberately factual only.
if (facts.isNotEmpty()) {
Text(facts.joinToString(FactSeparator), color = MutedText, fontSize = 13.sp,
maxLines = 1, overflow = TextOverflow.Ellipsis)
}
item.membyRecommendationReason?.takeIf(String::isNotBlank)?.let {
RecommendationReasonCaption(recommendationReasonSummary(it), Modifier.fillMaxWidth())
}
if (badges.isNotEmpty()) Row(horizontalArrangement = Arrangement.spacedBy(7.dp)) {
badges.forEach { MediaBadge(it) }
}
}
MetadataHeroSection.Genres -> if (genres.isNotBlank()) Text(
genres, color = Color.White, fontSize = 14.sp, fontWeight = FontWeight.Bold,
maxLines = 1, overflow = TextOverflow.Ellipsis,
)
MetadataHeroSection.Summary -> Text(
item.overview?.takeIf(String::isNotBlank) ?: "No description available.",
color = MembyMutedText, fontSize = 14.sp, lineHeight = 18.sp,
maxLines = if (compact) 2 else 3, overflow = TextOverflow.Ellipsis,
modifier = Modifier.fillMaxWidth(),
)
}
}
MetadataStatus(item)
}
}
private object MetadataHeroSection {
const val Title = "title"
const val Ratings = "ratings"
const val Facts = "facts"
const val Genres = "genres"
const val Summary = "summary"
val all = listOf(Title, Ratings, Facts, Genres, Summary)
}
internal fun metadataHeroContentOrder(order: List<String>): List<String> =
order.filter { it in MetadataHeroSection.all }.distinct()
.ifEmpty { MetadataHeroSection.all }
@Composable
private fun MetadataHeroTitle(item: BaseItem, logoUrl: String?, logo: String?, compact: Boolean) {
Box(
modifier = Modifier
.fillMaxWidth()
.then(
if (logoUrl != null) {
Modifier.height(MetadataHeroLogoMaxHeight)
} else {
Modifier.heightIn(max = MetadataHeroLogoMaxHeight)
},
modifier = Modifier.fillMaxWidth().then(
if (logoUrl != null) Modifier.height(MetadataHeroLogoMaxHeight)
else Modifier.heightIn(max = MetadataHeroLogoMaxHeight),
),
contentAlignment = Alignment.TopStart,
) {
@@ -299,82 +289,22 @@ private fun MetadataContent(
logoUrl = logo,
contentDescription = item.seriesName?.takeIf { item.isEpisode } ?: item.name,
alignment = Alignment.TopStart,
modifier = Modifier
.width(MetadataHeroLogoMaxWidth)
.height(MetadataHeroLogoMaxHeight),
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,
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.episodeCode
?.filter { it.isLetterOrDigit() }
?.lowercase(Locale.ROOT)
.orEmpty(),
color = MutedText, fontSize = 14.sp, maxLines = 1, overflow = TextOverflow.Ellipsis,
)
}
}
if (item.isEpisode && item.seriesName != null) {
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.productionYear?.let { add(it.toString()) }
item.runtimeMinutes?.let { add(formatRuntime(it)) }
item.officialRating?.takeIf(String::isNotBlank)?.let(::add)
item.genres.take(2).takeIf { it.isNotEmpty() }?.let { add(it.joinToString(ValueSeparator)) }
}
// No score on this line. Emby's community rating stood beside these facts in gold
// with nothing saying whose number it was; the strip below says that for every
// score it shows, and two ratings in one panel only invited the comparison.
if (facts.isNotEmpty()) {
Text(
facts.joinToString(FactSeparator),
color = MutedText,
fontSize = 13.sp,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
}
ItemRatingsStrip(
item = item,
load = true,
compact = true,
modifier = Modifier.fillMaxWidth(),
)
val badges = buildList {
airingBadgeLabel(item)?.let(::add)
addAll(mediaBadges(item))
}
item.membyRecommendationReason?.takeIf(String::isNotBlank)?.let {
RecommendationReasonCaption(
text = recommendationReasonSummary(it),
modifier = Modifier.fillMaxWidth(),
)
}
if (badges.isNotEmpty()) {
Row(horizontalArrangement = Arrangement.spacedBy(7.dp)) {
badges.forEach { MediaBadge(it) }
}
}
Text(
item.overview?.takeIf(String::isNotBlank) ?: "No description available.",
color = MembyMutedText,
fontSize = 14.sp,
lineHeight = 18.sp,
maxLines = if (compact) 2 else 3,
overflow = TextOverflow.Ellipsis,
modifier = Modifier.fillMaxWidth(),
)
MetadataStatus(item)
}
}
@Composable
private fun ScheduleMetadataContent(
@@ -440,7 +370,7 @@ private fun ScheduleMetadataContent(
modifier = Modifier.fillMaxWidth(),
)
Text(
if (item.isMovieSchedule) "Upcoming digital releases" else "TV this week",
if (item.isMovieSchedule) "Upcoming Movies" else "Upcoming TV",
color = QuietText,
fontSize = 12.sp,
)
@@ -25,6 +25,7 @@ class UserPreferencesTest {
homeArtworkStyle = "poster",
showHomeCardMetadata = false,
showRatingsStrip = false,
metadataHeroContentOrder = listOf("title", "ratings", "facts", "summary", "genres"),
hideWatchedMovies = true,
showTitleLogo = false,
welcomeQuoteStyle = "homicidal",
@@ -142,6 +143,7 @@ class UserPreferencesTest {
homeArtworkStyle = "backdrop",
showHomeCardMetadata = false,
showRatingsStrip = false,
metadataHeroContentOrder = "title\nratings\nfacts\ngenres\nsummary",
hideWatchedMovies = true,
showTitleLogo = false,
welcomeQuoteStyle = "positive",
@@ -162,6 +164,7 @@ class UserPreferencesTest {
homeArtworkStyle = "backdrop",
showHomeCardMetadata = false,
showRatingsStrip = false,
metadataHeroContentOrder = listOf("title", "ratings", "facts", "genres", "summary"),
hideWatchedMovies = true,
showTitleLogo = false,
welcomeQuoteStyle = "positive",
@@ -0,0 +1,23 @@
package com.ponzischeme89.memby.ui
import org.junit.Assert.assertEquals
import org.junit.Test
class MetadataHeroOrderTest {
@Test
fun `server order is retained exactly`() {
assertEquals(
listOf("ratings", "title"),
metadataHeroContentOrder(listOf("ratings", "title")),
)
}
@Test
fun `unknown sections fall back to the default layout`() {
assertEquals(
listOf("title", "ratings", "facts", "genres", "summary"),
metadataHeroContentOrder(listOf("unknown")),
)
}
}
+15
View File
@@ -130,6 +130,21 @@ var preferenceCatalogue = []preferenceDefinition{
Description: "Show third-party ratings on browse and detail pages.",
Kind: preferenceToggle, Default: true,
},
{
// This is operator-owned because the hero is a shared piece of the launcher's
// composition, not a per-television preference. The multi value keeps its order.
Key: "metadataHeroContentOrder", Name: "Metadata hero content order", Area: "Home layout",
Description: "The order of title, ratings, facts, genre and plot summary in the focused metadata hero.",
Kind: preferenceMulti, AdminOnly: true,
Default: []string{"title", "ratings", "facts", "genres", "summary"},
Options: []preferenceOption{
option("title", "Movie logo or title"),
option("ratings", "Ratings"),
option("facts", "Metadata facts"),
option("genres", "Genre"),
option("summary", "Plot summary"),
},
},
{
Key: "hideWatchedMovies", Name: "Hide watched films", Area: "Home",
Description: "Keep fully watched films out of browse rows and the hero.",