0.2.94
This commit is contained in:
@@ -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,104 +213,99 @@ 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),
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.then(
|
||||
if (logoUrl != null) {
|
||||
Modifier.height(MetadataHeroLogoMaxHeight)
|
||||
} else {
|
||||
Modifier.heightIn(max = MetadataHeroLogoMaxHeight)
|
||||
},
|
||||
),
|
||||
contentAlignment = Alignment.TopStart,
|
||||
) {
|
||||
if (logo != null) {
|
||||
TitleLogoImage(
|
||||
logoUrl = logo,
|
||||
contentDescription = item.seriesName?.takeIf { item.isEpisode } ?: item.name,
|
||||
alignment = Alignment.TopStart,
|
||||
modifier = Modifier
|
||||
.width(MetadataHeroLogoMaxWidth)
|
||||
.height(MetadataHeroLogoMaxHeight),
|
||||
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(),
|
||||
)
|
||||
} 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,
|
||||
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(),
|
||||
)
|
||||
}
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
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),
|
||||
),
|
||||
contentAlignment = Alignment.TopStart,
|
||||
) {
|
||||
if (logo != null) {
|
||||
TitleLogoImage(
|
||||
logoUrl = logo,
|
||||
contentDescription = item.seriesName?.takeIf { item.isEpisode } ?: item.name,
|
||||
alignment = Alignment.TopStart,
|
||||
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.episodeCode
|
||||
?.filter { it.isLetterOrDigit() }
|
||||
?.lowercase(Locale.ROOT)
|
||||
.orEmpty(),
|
||||
color = MutedText, fontSize = 14.sp, maxLines = 1, overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ScheduleMetadataContent(
|
||||
item: BaseItem,
|
||||
@@ -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")),
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user