mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-05-17 15:32:01 +00:00
feat: label control
This commit is contained in:
parent
2d1ab47919
commit
e373759de7
7 changed files with 305 additions and 123 deletions
|
|
@ -109,7 +109,11 @@ fun NuvioPosterCard(
|
||||||
val posterCardStyle = rememberPosterCardStyleUiState()
|
val posterCardStyle = rememberPosterCardStyleUiState()
|
||||||
val cardWidth = shape.cardWidth(basePosterWidthDp = posterCardStyle.widthDp)
|
val cardWidth = shape.cardWidth(basePosterWidthDp = posterCardStyle.widthDp)
|
||||||
val cardShape = RoundedCornerShape(posterCardStyle.cornerRadiusDp.dp)
|
val cardShape = RoundedCornerShape(posterCardStyle.cornerRadiusDp.dp)
|
||||||
val catalogLogoOverlaySize = catalogLogoOverlaySize(basePosterWidthDp = posterCardStyle.widthDp)
|
val catalogLogoOverlaySize = catalogLogoOverlaySize(
|
||||||
|
basePosterWidthDp = posterCardStyle.widthDp,
|
||||||
|
shape = shape,
|
||||||
|
)
|
||||||
|
val shouldShowTitleBelow = showTitleBelow && !posterCardStyle.hideLabelsEnabled
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
modifier = modifier.width(cardWidth),
|
modifier = modifier.width(cardWidth),
|
||||||
|
|
@ -178,7 +182,7 @@ fun NuvioPosterCard(
|
||||||
.padding(6.dp),
|
.padding(6.dp),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (showTitleBelow) {
|
if (shouldShowTitleBelow) {
|
||||||
Text(
|
Text(
|
||||||
text = title,
|
text = title,
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
|
@ -302,12 +306,24 @@ private data class CatalogLogoOverlaySize(
|
||||||
val textMaxWidth: Dp,
|
val textMaxWidth: Dp,
|
||||||
)
|
)
|
||||||
|
|
||||||
private fun catalogLogoOverlaySize(basePosterWidthDp: Int): CatalogLogoOverlaySize =
|
private fun catalogLogoOverlaySize(
|
||||||
when {
|
basePosterWidthDp: Int,
|
||||||
basePosterWidthDp <= 108 -> CatalogLogoOverlaySize(width = 72.dp, height = 18.dp, textMaxWidth = 92.dp)
|
shape: NuvioPosterShape,
|
||||||
basePosterWidthDp <= 120 -> CatalogLogoOverlaySize(width = 80.dp, height = 20.dp, textMaxWidth = 104.dp)
|
): CatalogLogoOverlaySize =
|
||||||
basePosterWidthDp <= 132 -> CatalogLogoOverlaySize(width = 88.dp, height = 22.dp, textMaxWidth = 112.dp)
|
if (shape == NuvioPosterShape.Landscape) {
|
||||||
else -> CatalogLogoOverlaySize(width = 96.dp, height = 24.dp, textMaxWidth = 124.dp)
|
when {
|
||||||
|
basePosterWidthDp <= 108 -> CatalogLogoOverlaySize(width = 92.dp, height = 24.dp, textMaxWidth = 120.dp)
|
||||||
|
basePosterWidthDp <= 120 -> CatalogLogoOverlaySize(width = 104.dp, height = 28.dp, textMaxWidth = 132.dp)
|
||||||
|
basePosterWidthDp <= 132 -> CatalogLogoOverlaySize(width = 116.dp, height = 30.dp, textMaxWidth = 144.dp)
|
||||||
|
else -> CatalogLogoOverlaySize(width = 128.dp, height = 34.dp, textMaxWidth = 156.dp)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
when {
|
||||||
|
basePosterWidthDp <= 108 -> CatalogLogoOverlaySize(width = 72.dp, height = 18.dp, textMaxWidth = 92.dp)
|
||||||
|
basePosterWidthDp <= 120 -> CatalogLogoOverlaySize(width = 80.dp, height = 20.dp, textMaxWidth = 104.dp)
|
||||||
|
basePosterWidthDp <= 132 -> CatalogLogoOverlaySize(width = 88.dp, height = 22.dp, textMaxWidth = 112.dp)
|
||||||
|
else -> CatalogLogoOverlaySize(width = 96.dp, height = 24.dp, textMaxWidth = 124.dp)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun NuvioPosterShape.cardWidth(basePosterWidthDp: Int): Dp =
|
private fun NuvioPosterShape.cardWidth(basePosterWidthDp: Int): Dp =
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ private data class StoredPosterCardStylePreferences(
|
||||||
val heightDp: Int = DefaultPosterCardHeightDp,
|
val heightDp: Int = DefaultPosterCardHeightDp,
|
||||||
val cornerRadiusDp: Int = DefaultPosterCardCornerRadiusDp,
|
val cornerRadiusDp: Int = DefaultPosterCardCornerRadiusDp,
|
||||||
val catalogLandscapeModeEnabled: Boolean = false,
|
val catalogLandscapeModeEnabled: Boolean = false,
|
||||||
|
val hideLabelsEnabled: Boolean = false,
|
||||||
)
|
)
|
||||||
|
|
||||||
data class PosterCardStyleUiState(
|
data class PosterCardStyleUiState(
|
||||||
|
|
@ -25,6 +26,7 @@ data class PosterCardStyleUiState(
|
||||||
val heightDp: Int = DefaultPosterCardHeightDp,
|
val heightDp: Int = DefaultPosterCardHeightDp,
|
||||||
val cornerRadiusDp: Int = DefaultPosterCardCornerRadiusDp,
|
val cornerRadiusDp: Int = DefaultPosterCardCornerRadiusDp,
|
||||||
val catalogLandscapeModeEnabled: Boolean = false,
|
val catalogLandscapeModeEnabled: Boolean = false,
|
||||||
|
val hideLabelsEnabled: Boolean = false,
|
||||||
)
|
)
|
||||||
|
|
||||||
object PosterCardStyleRepository {
|
object PosterCardStyleRepository {
|
||||||
|
|
@ -78,6 +80,13 @@ object PosterCardStyleRepository {
|
||||||
persist()
|
persist()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun setHideLabelsEnabled(enabled: Boolean) {
|
||||||
|
ensureLoaded()
|
||||||
|
if (_uiState.value.hideLabelsEnabled == enabled) return
|
||||||
|
_uiState.value = _uiState.value.copy(hideLabelsEnabled = enabled)
|
||||||
|
persist()
|
||||||
|
}
|
||||||
|
|
||||||
fun resetToDefaults() {
|
fun resetToDefaults() {
|
||||||
ensureLoaded()
|
ensureLoaded()
|
||||||
if (_uiState.value == PosterCardStyleUiState()) return
|
if (_uiState.value == PosterCardStyleUiState()) return
|
||||||
|
|
@ -107,6 +116,7 @@ object PosterCardStyleRepository {
|
||||||
heightDp = heightDp,
|
heightDp = heightDp,
|
||||||
cornerRadiusDp = cornerRadiusDp,
|
cornerRadiusDp = cornerRadiusDp,
|
||||||
catalogLandscapeModeEnabled = stored.catalogLandscapeModeEnabled,
|
catalogLandscapeModeEnabled = stored.catalogLandscapeModeEnabled,
|
||||||
|
hideLabelsEnabled = stored.hideLabelsEnabled,
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
PosterCardStyleUiState()
|
PosterCardStyleUiState()
|
||||||
|
|
@ -121,6 +131,7 @@ object PosterCardStyleRepository {
|
||||||
heightDp = _uiState.value.heightDp,
|
heightDp = _uiState.value.heightDp,
|
||||||
cornerRadiusDp = _uiState.value.cornerRadiusDp,
|
cornerRadiusDp = _uiState.value.cornerRadiusDp,
|
||||||
catalogLandscapeModeEnabled = _uiState.value.catalogLandscapeModeEnabled,
|
catalogLandscapeModeEnabled = _uiState.value.catalogLandscapeModeEnabled,
|
||||||
|
hideLabelsEnabled = _uiState.value.hideLabelsEnabled,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -179,6 +179,7 @@ fun CatalogScreen(
|
||||||
CatalogPosterTile(
|
CatalogPosterTile(
|
||||||
item = item,
|
item = item,
|
||||||
cornerRadiusDp = posterCardStyle.cornerRadiusDp,
|
cornerRadiusDp = posterCardStyle.cornerRadiusDp,
|
||||||
|
hideLabels = posterCardStyle.hideLabelsEnabled,
|
||||||
onClick = onPosterClick?.let { { it(item) } },
|
onClick = onPosterClick?.let { { it(item) } },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -248,6 +249,7 @@ private fun CatalogHeader(
|
||||||
private fun CatalogPosterTile(
|
private fun CatalogPosterTile(
|
||||||
item: MetaPreview,
|
item: MetaPreview,
|
||||||
cornerRadiusDp: Int,
|
cornerRadiusDp: Int,
|
||||||
|
hideLabels: Boolean,
|
||||||
onClick: (() -> Unit)? = null,
|
onClick: (() -> Unit)? = null,
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
|
|
@ -270,24 +272,26 @@ private fun CatalogPosterTile(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Text(
|
if (!hideLabels) {
|
||||||
text = item.name,
|
|
||||||
style = MaterialTheme.typography.titleMedium.copy(fontWeight = FontWeight.SemiBold),
|
|
||||||
color = MaterialTheme.colorScheme.onBackground,
|
|
||||||
maxLines = 2,
|
|
||||||
overflow = TextOverflow.Ellipsis,
|
|
||||||
)
|
|
||||||
val detail = item.releaseInfo?.let { formatReleaseDateForDisplay(it) }
|
|
||||||
if (detail != null) {
|
|
||||||
Text(
|
Text(
|
||||||
text = detail,
|
text = item.name,
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
style = MaterialTheme.typography.titleMedium.copy(fontWeight = FontWeight.SemiBold),
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
color = MaterialTheme.colorScheme.onBackground,
|
||||||
maxLines = 1,
|
maxLines = 2,
|
||||||
overflow = TextOverflow.Ellipsis,
|
overflow = TextOverflow.Ellipsis,
|
||||||
)
|
)
|
||||||
} else {
|
val detail = item.releaseInfo?.let { formatReleaseDateForDisplay(it) }
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
if (detail != null) {
|
||||||
|
Text(
|
||||||
|
text = detail,
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -26,10 +26,10 @@ fun HomePosterCard(
|
||||||
imageUrl = if (isLandscapeMode) (item.banner ?: item.poster) else item.poster,
|
imageUrl = if (isLandscapeMode) (item.banner ?: item.poster) else item.poster,
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
shape = if (isLandscapeMode) NuvioPosterShape.Landscape else item.posterShape.toNuvioPosterShape(),
|
shape = if (isLandscapeMode) NuvioPosterShape.Landscape else item.posterShape.toNuvioPosterShape(),
|
||||||
detailLine = if (isLandscapeMode) null else item.releaseInfo?.let { formatReleaseDateForDisplay(it) },
|
detailLine = if (isLandscapeMode || posterCardStyle.hideLabelsEnabled) null else item.releaseInfo?.let { formatReleaseDateForDisplay(it) },
|
||||||
showTitleBelow = !isLandscapeMode,
|
showTitleBelow = !posterCardStyle.hideLabelsEnabled,
|
||||||
bottomLeftLogoUrl = if (isLandscapeMode) item.logo else null,
|
bottomLeftLogoUrl = if (isLandscapeMode) item.logo else null,
|
||||||
bottomLeftText = if (isLandscapeMode && item.logo.isNullOrBlank()) item.name else null,
|
bottomLeftText = if (isLandscapeMode && item.logo.isNullOrBlank() && !posterCardStyle.hideLabelsEnabled) item.name else null,
|
||||||
isWatched = isWatched,
|
isWatched = isWatched,
|
||||||
onClick = onClick,
|
onClick = onClick,
|
||||||
onLongClick = onLongClick,
|
onLongClick = onLongClick,
|
||||||
|
|
|
||||||
|
|
@ -350,6 +350,7 @@ private fun DiscoverGridRow(
|
||||||
DiscoverPosterTile(
|
DiscoverPosterTile(
|
||||||
item = item,
|
item = item,
|
||||||
cornerRadiusDp = posterCardStyle.cornerRadiusDp,
|
cornerRadiusDp = posterCardStyle.cornerRadiusDp,
|
||||||
|
hideLabels = posterCardStyle.hideLabelsEnabled,
|
||||||
modifier = Modifier.weight(1f),
|
modifier = Modifier.weight(1f),
|
||||||
isWatched = WatchingState.isPosterWatched(
|
isWatched = WatchingState.isPosterWatched(
|
||||||
watchedKeys = watchedKeys,
|
watchedKeys = watchedKeys,
|
||||||
|
|
@ -370,6 +371,7 @@ private fun DiscoverGridRow(
|
||||||
private fun DiscoverPosterTile(
|
private fun DiscoverPosterTile(
|
||||||
item: MetaPreview,
|
item: MetaPreview,
|
||||||
cornerRadiusDp: Int,
|
cornerRadiusDp: Int,
|
||||||
|
hideLabels: Boolean,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
isWatched: Boolean = false,
|
isWatched: Boolean = false,
|
||||||
onClick: (() -> Unit)? = null,
|
onClick: (() -> Unit)? = null,
|
||||||
|
|
@ -402,24 +404,26 @@ private fun DiscoverPosterTile(
|
||||||
.padding(6.dp),
|
.padding(6.dp),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
Text(
|
if (!hideLabels) {
|
||||||
text = item.name,
|
|
||||||
style = MaterialTheme.typography.titleMedium.copy(fontWeight = FontWeight.SemiBold),
|
|
||||||
color = MaterialTheme.colorScheme.onBackground,
|
|
||||||
maxLines = 2,
|
|
||||||
overflow = TextOverflow.Ellipsis,
|
|
||||||
)
|
|
||||||
val detail = item.releaseInfo?.let { formatReleaseDateForDisplay(it) }
|
|
||||||
if (detail != null) {
|
|
||||||
Text(
|
Text(
|
||||||
text = detail,
|
text = item.name,
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
style = MaterialTheme.typography.titleMedium.copy(fontWeight = FontWeight.SemiBold),
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
color = MaterialTheme.colorScheme.onBackground,
|
||||||
maxLines = 1,
|
maxLines = 2,
|
||||||
overflow = TextOverflow.Ellipsis,
|
overflow = TextOverflow.Ellipsis,
|
||||||
)
|
)
|
||||||
} else {
|
val detail = item.releaseInfo?.let { formatReleaseDateForDisplay(it) }
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
if (detail != null) {
|
||||||
|
Text(
|
||||||
|
text = detail,
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ package com.nuvio.app.features.settings
|
||||||
import androidx.compose.animation.core.animateDpAsState
|
import androidx.compose.animation.core.animateDpAsState
|
||||||
import androidx.compose.animation.core.tween
|
import androidx.compose.animation.core.tween
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.border
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
|
|
@ -54,9 +55,11 @@ internal fun LazyListScope.posterCustomizationSettingsContent(
|
||||||
widthDp = uiState.widthDp,
|
widthDp = uiState.widthDp,
|
||||||
cornerRadiusDp = uiState.cornerRadiusDp,
|
cornerRadiusDp = uiState.cornerRadiusDp,
|
||||||
catalogLandscapeModeEnabled = uiState.catalogLandscapeModeEnabled,
|
catalogLandscapeModeEnabled = uiState.catalogLandscapeModeEnabled,
|
||||||
|
hideLabelsEnabled = uiState.hideLabelsEnabled,
|
||||||
onWidthSelected = PosterCardStyleRepository::setWidthDp,
|
onWidthSelected = PosterCardStyleRepository::setWidthDp,
|
||||||
onCornerRadiusSelected = PosterCardStyleRepository::setCornerRadiusDp,
|
onCornerRadiusSelected = PosterCardStyleRepository::setCornerRadiusDp,
|
||||||
onCatalogLandscapeModeChange = PosterCardStyleRepository::setCatalogLandscapeModeEnabled,
|
onCatalogLandscapeModeChange = PosterCardStyleRepository::setCatalogLandscapeModeEnabled,
|
||||||
|
onHideLabelsChange = PosterCardStyleRepository::setHideLabelsEnabled,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -70,9 +73,11 @@ private fun PosterCardStyleControls(
|
||||||
widthDp: Int,
|
widthDp: Int,
|
||||||
cornerRadiusDp: Int,
|
cornerRadiusDp: Int,
|
||||||
catalogLandscapeModeEnabled: Boolean,
|
catalogLandscapeModeEnabled: Boolean,
|
||||||
|
hideLabelsEnabled: Boolean,
|
||||||
onWidthSelected: (Int) -> Unit,
|
onWidthSelected: (Int) -> Unit,
|
||||||
onCornerRadiusSelected: (Int) -> Unit,
|
onCornerRadiusSelected: (Int) -> Unit,
|
||||||
onCatalogLandscapeModeChange: (Boolean) -> Unit,
|
onCatalogLandscapeModeChange: (Boolean) -> Unit,
|
||||||
|
onHideLabelsChange: (Boolean) -> Unit,
|
||||||
) {
|
) {
|
||||||
val widthOptions = listOf(
|
val widthOptions = listOf(
|
||||||
PresetOption("Compact", 104),
|
PresetOption("Compact", 104),
|
||||||
|
|
@ -121,6 +126,11 @@ private fun PosterCardStyleControls(
|
||||||
checked = catalogLandscapeModeEnabled,
|
checked = catalogLandscapeModeEnabled,
|
||||||
onCheckedChange = onCatalogLandscapeModeChange,
|
onCheckedChange = onCatalogLandscapeModeChange,
|
||||||
)
|
)
|
||||||
|
PosterToggleRow(
|
||||||
|
title = "Hide labels",
|
||||||
|
checked = hideLabelsEnabled,
|
||||||
|
onCheckedChange = onHideLabelsChange,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -128,6 +138,19 @@ private fun PosterCardStyleControls(
|
||||||
private fun PosterLandscapeModeToggleRow(
|
private fun PosterLandscapeModeToggleRow(
|
||||||
checked: Boolean,
|
checked: Boolean,
|
||||||
onCheckedChange: (Boolean) -> Unit,
|
onCheckedChange: (Boolean) -> Unit,
|
||||||
|
) {
|
||||||
|
PosterToggleRow(
|
||||||
|
title = "Landscape mode for shelf posters",
|
||||||
|
checked = checked,
|
||||||
|
onCheckedChange = onCheckedChange,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun PosterToggleRow(
|
||||||
|
title: String,
|
||||||
|
checked: Boolean,
|
||||||
|
onCheckedChange: (Boolean) -> Unit,
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
|
@ -135,7 +158,7 @@ private fun PosterLandscapeModeToggleRow(
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = "Landscape mode for shelf posters",
|
text = title,
|
||||||
style = MaterialTheme.typography.bodyLarge,
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
color = MaterialTheme.colorScheme.onSurface,
|
color = MaterialTheme.colorScheme.onSurface,
|
||||||
fontWeight = FontWeight.Medium,
|
fontWeight = FontWeight.Medium,
|
||||||
|
|
@ -203,26 +226,13 @@ private fun PosterCardLivePreview(
|
||||||
.width(animatedWidth.value)
|
.width(animatedWidth.value)
|
||||||
.height(animatedHeight.value)
|
.height(animatedHeight.value)
|
||||||
.clip(RoundedCornerShape(animatedCornerRadius.value))
|
.clip(RoundedCornerShape(animatedCornerRadius.value))
|
||||||
.background(MaterialTheme.colorScheme.surfaceVariant),
|
.background(MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.45f))
|
||||||
) {
|
.border(
|
||||||
Box(
|
width = 1.dp,
|
||||||
modifier = Modifier
|
color = MaterialTheme.colorScheme.outlineVariant,
|
||||||
.align(Alignment.TopStart)
|
shape = RoundedCornerShape(animatedCornerRadius.value),
|
||||||
.padding(10.dp)
|
),
|
||||||
.size(34.dp)
|
)
|
||||||
.clip(RoundedCornerShape(999.dp))
|
|
||||||
.background(MaterialTheme.colorScheme.primary.copy(alpha = 0.2f)),
|
|
||||||
)
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.align(Alignment.BottomStart)
|
|
||||||
.padding(10.dp)
|
|
||||||
.width(70.dp)
|
|
||||||
.height(7.dp)
|
|
||||||
.clip(RoundedCornerShape(999.dp))
|
|
||||||
.background(MaterialTheme.colorScheme.onSurface.copy(alpha = 0.22f)),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,7 @@ object TmdbMetadataService {
|
||||||
private val entityBrowseCache = mutableMapOf<String, TmdbEntityBrowseData>()
|
private val entityBrowseCache = mutableMapOf<String, TmdbEntityBrowseData>()
|
||||||
private val entityHeaderCache = mutableMapOf<String, TmdbEntityHeader>()
|
private val entityHeaderCache = mutableMapOf<String, TmdbEntityHeader>()
|
||||||
private val entityRailCache = mutableMapOf<String, List<MetaPreview>>()
|
private val entityRailCache = mutableMapOf<String, List<MetaPreview>>()
|
||||||
|
private val previewArtworkCache = mutableMapOf<String, TmdbPreviewArtwork>()
|
||||||
|
|
||||||
suspend fun fetchPersonDetail(
|
suspend fun fetchPersonDetail(
|
||||||
personId: Int,
|
personId: Int,
|
||||||
|
|
@ -75,16 +76,16 @@ object TmdbMetadataService {
|
||||||
|
|
||||||
val preferCrew = preferCrewCredits ?: shouldPreferCrewCredits(person.knownForDepartment)
|
val preferCrew = preferCrewCredits ?: shouldPreferCrewCredits(person.knownForDepartment)
|
||||||
|
|
||||||
val castMovieCredits = mapPersonMovieCreditsFromCast(credits?.cast.orEmpty())
|
val castMovieCredits = mapPersonMovieCreditsFromCast(credits?.cast.orEmpty(), language)
|
||||||
val crewMovieCredits = mapPersonMovieCreditsFromCrew(credits?.crew.orEmpty())
|
val crewMovieCredits = mapPersonMovieCreditsFromCrew(credits?.crew.orEmpty(), language)
|
||||||
val movieCredits = when {
|
val movieCredits = when {
|
||||||
preferCrew && crewMovieCredits.isNotEmpty() -> crewMovieCredits
|
preferCrew && crewMovieCredits.isNotEmpty() -> crewMovieCredits
|
||||||
castMovieCredits.isNotEmpty() -> castMovieCredits
|
castMovieCredits.isNotEmpty() -> castMovieCredits
|
||||||
else -> crewMovieCredits
|
else -> crewMovieCredits
|
||||||
}
|
}
|
||||||
|
|
||||||
val castTvCredits = mapPersonTvCreditsFromCast(credits?.cast.orEmpty())
|
val castTvCredits = mapPersonTvCreditsFromCast(credits?.cast.orEmpty(), language)
|
||||||
val crewTvCredits = mapPersonTvCreditsFromCrew(credits?.crew.orEmpty())
|
val crewTvCredits = mapPersonTvCreditsFromCrew(credits?.crew.orEmpty(), language)
|
||||||
val tvCredits = when {
|
val tvCredits = when {
|
||||||
preferCrew && crewTvCredits.isNotEmpty() -> crewTvCredits
|
preferCrew && crewTvCredits.isNotEmpty() -> crewTvCredits
|
||||||
castTvCredits.isNotEmpty() -> castTvCredits
|
castTvCredits.isNotEmpty() -> castTvCredits
|
||||||
|
|
@ -116,88 +117,160 @@ object TmdbMetadataService {
|
||||||
return department.isNotBlank() && department != "acting" && department != "actors"
|
return department.isNotBlank() && department != "acting" && department != "actors"
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun mapPersonMovieCreditsFromCast(cast: List<TmdbPersonCreditCast>): List<MetaPreview> {
|
private suspend fun mapPersonMovieCreditsFromCast(
|
||||||
|
cast: List<TmdbPersonCreditCast>,
|
||||||
|
language: String,
|
||||||
|
): List<MetaPreview> = coroutineScope {
|
||||||
val seen = mutableSetOf<Int>()
|
val seen = mutableSetOf<Int>()
|
||||||
return cast
|
cast
|
||||||
.filter { it.mediaType == "movie" && it.posterPath != null }
|
.filter { it.mediaType == "movie" && (it.posterPath != null || it.backdropPath != null) }
|
||||||
.sortedByDescending { it.voteAverage ?: 0.0 }
|
.sortedByDescending { it.voteAverage ?: 0.0 }
|
||||||
.mapNotNull { credit ->
|
.mapNotNull { credit ->
|
||||||
if (!seen.add(credit.id)) return@mapNotNull null
|
if (!seen.add(credit.id)) return@mapNotNull null
|
||||||
val title = credit.title ?: credit.name ?: return@mapNotNull null
|
val title = credit.title ?: credit.name ?: return@mapNotNull null
|
||||||
MetaPreview(
|
async {
|
||||||
id = "tmdb:${credit.id}",
|
val artwork = fetchPreviewArtwork(
|
||||||
type = "movie",
|
tmdbId = credit.id,
|
||||||
name = title,
|
mediaType = "movie",
|
||||||
poster = buildImageUrl(credit.posterPath, "w500"),
|
language = language,
|
||||||
description = credit.overview?.takeIf { it.isNotBlank() },
|
)
|
||||||
releaseInfo = credit.releaseDate?.take(4),
|
val poster = buildImageUrl(credit.posterPath, "w500")
|
||||||
rawReleaseDate = credit.releaseDate,
|
?: buildImageUrl(credit.backdropPath, "w780")
|
||||||
popularity = credit.popularity,
|
?: artwork?.backdrop
|
||||||
)
|
?: return@async null
|
||||||
|
MetaPreview(
|
||||||
|
id = "tmdb:${credit.id}",
|
||||||
|
type = "movie",
|
||||||
|
name = title,
|
||||||
|
poster = poster,
|
||||||
|
banner = buildImageUrl(credit.backdropPath, "w780") ?: artwork?.backdrop,
|
||||||
|
logo = artwork?.logo,
|
||||||
|
description = credit.overview?.takeIf { it.isNotBlank() },
|
||||||
|
releaseInfo = credit.releaseDate?.take(4),
|
||||||
|
rawReleaseDate = credit.releaseDate,
|
||||||
|
popularity = credit.popularity,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
.awaitAll()
|
||||||
|
.filterNotNull()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun mapPersonMovieCreditsFromCrew(crew: List<TmdbPersonCreditCrew>): List<MetaPreview> {
|
private suspend fun mapPersonMovieCreditsFromCrew(
|
||||||
|
crew: List<TmdbPersonCreditCrew>,
|
||||||
|
language: String,
|
||||||
|
): List<MetaPreview> = coroutineScope {
|
||||||
val seen = mutableSetOf<Int>()
|
val seen = mutableSetOf<Int>()
|
||||||
return crew
|
crew
|
||||||
.filter { it.mediaType == "movie" && it.posterPath != null }
|
.filter { it.mediaType == "movie" && (it.posterPath != null || it.backdropPath != null) }
|
||||||
.sortedByDescending { it.voteAverage ?: 0.0 }
|
.sortedByDescending { it.voteAverage ?: 0.0 }
|
||||||
.mapNotNull { credit ->
|
.mapNotNull { credit ->
|
||||||
if (!seen.add(credit.id)) return@mapNotNull null
|
if (!seen.add(credit.id)) return@mapNotNull null
|
||||||
val title = credit.title ?: credit.name ?: return@mapNotNull null
|
val title = credit.title ?: credit.name ?: return@mapNotNull null
|
||||||
MetaPreview(
|
async {
|
||||||
id = "tmdb:${credit.id}",
|
val artwork = fetchPreviewArtwork(
|
||||||
type = "movie",
|
tmdbId = credit.id,
|
||||||
name = title,
|
mediaType = "movie",
|
||||||
poster = buildImageUrl(credit.posterPath, "w500"),
|
language = language,
|
||||||
description = credit.overview?.takeIf { it.isNotBlank() },
|
)
|
||||||
releaseInfo = credit.releaseDate?.take(4),
|
val poster = buildImageUrl(credit.posterPath, "w500")
|
||||||
rawReleaseDate = credit.releaseDate,
|
?: buildImageUrl(credit.backdropPath, "w780")
|
||||||
popularity = credit.popularity,
|
?: artwork?.backdrop
|
||||||
)
|
?: return@async null
|
||||||
|
MetaPreview(
|
||||||
|
id = "tmdb:${credit.id}",
|
||||||
|
type = "movie",
|
||||||
|
name = title,
|
||||||
|
poster = poster,
|
||||||
|
banner = buildImageUrl(credit.backdropPath, "w780") ?: artwork?.backdrop,
|
||||||
|
logo = artwork?.logo,
|
||||||
|
description = credit.overview?.takeIf { it.isNotBlank() },
|
||||||
|
releaseInfo = credit.releaseDate?.take(4),
|
||||||
|
rawReleaseDate = credit.releaseDate,
|
||||||
|
popularity = credit.popularity,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
.awaitAll()
|
||||||
|
.filterNotNull()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun mapPersonTvCreditsFromCast(cast: List<TmdbPersonCreditCast>): List<MetaPreview> {
|
private suspend fun mapPersonTvCreditsFromCast(
|
||||||
|
cast: List<TmdbPersonCreditCast>,
|
||||||
|
language: String,
|
||||||
|
): List<MetaPreview> = coroutineScope {
|
||||||
val seen = mutableSetOf<Int>()
|
val seen = mutableSetOf<Int>()
|
||||||
return cast
|
cast
|
||||||
.filter { it.mediaType == "tv" && it.posterPath != null }
|
.filter { it.mediaType == "tv" && (it.posterPath != null || it.backdropPath != null) }
|
||||||
.sortedByDescending { it.voteAverage ?: 0.0 }
|
.sortedByDescending { it.voteAverage ?: 0.0 }
|
||||||
.mapNotNull { credit ->
|
.mapNotNull { credit ->
|
||||||
if (!seen.add(credit.id)) return@mapNotNull null
|
if (!seen.add(credit.id)) return@mapNotNull null
|
||||||
val title = credit.name ?: credit.title ?: return@mapNotNull null
|
val title = credit.name ?: credit.title ?: return@mapNotNull null
|
||||||
MetaPreview(
|
async {
|
||||||
id = "tmdb:${credit.id}",
|
val artwork = fetchPreviewArtwork(
|
||||||
type = "series",
|
tmdbId = credit.id,
|
||||||
name = title,
|
mediaType = "tv",
|
||||||
poster = buildImageUrl(credit.posterPath, "w500"),
|
language = language,
|
||||||
description = credit.overview?.takeIf { it.isNotBlank() },
|
)
|
||||||
releaseInfo = credit.firstAirDate?.take(4),
|
val poster = buildImageUrl(credit.posterPath, "w500")
|
||||||
rawReleaseDate = credit.firstAirDate,
|
?: buildImageUrl(credit.backdropPath, "w780")
|
||||||
popularity = credit.popularity,
|
?: artwork?.backdrop
|
||||||
)
|
?: return@async null
|
||||||
|
MetaPreview(
|
||||||
|
id = "tmdb:${credit.id}",
|
||||||
|
type = "series",
|
||||||
|
name = title,
|
||||||
|
poster = poster,
|
||||||
|
banner = buildImageUrl(credit.backdropPath, "w780") ?: artwork?.backdrop,
|
||||||
|
logo = artwork?.logo,
|
||||||
|
description = credit.overview?.takeIf { it.isNotBlank() },
|
||||||
|
releaseInfo = credit.firstAirDate?.take(4),
|
||||||
|
rawReleaseDate = credit.firstAirDate,
|
||||||
|
popularity = credit.popularity,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
.awaitAll()
|
||||||
|
.filterNotNull()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun mapPersonTvCreditsFromCrew(crew: List<TmdbPersonCreditCrew>): List<MetaPreview> {
|
private suspend fun mapPersonTvCreditsFromCrew(
|
||||||
|
crew: List<TmdbPersonCreditCrew>,
|
||||||
|
language: String,
|
||||||
|
): List<MetaPreview> = coroutineScope {
|
||||||
val seen = mutableSetOf<Int>()
|
val seen = mutableSetOf<Int>()
|
||||||
return crew
|
crew
|
||||||
.filter { it.mediaType == "tv" && it.posterPath != null }
|
.filter { it.mediaType == "tv" && (it.posterPath != null || it.backdropPath != null) }
|
||||||
.sortedByDescending { it.voteAverage ?: 0.0 }
|
.sortedByDescending { it.voteAverage ?: 0.0 }
|
||||||
.mapNotNull { credit ->
|
.mapNotNull { credit ->
|
||||||
if (!seen.add(credit.id)) return@mapNotNull null
|
if (!seen.add(credit.id)) return@mapNotNull null
|
||||||
val title = credit.name ?: credit.title ?: return@mapNotNull null
|
val title = credit.name ?: credit.title ?: return@mapNotNull null
|
||||||
MetaPreview(
|
async {
|
||||||
id = "tmdb:${credit.id}",
|
val artwork = fetchPreviewArtwork(
|
||||||
type = "series",
|
tmdbId = credit.id,
|
||||||
name = title,
|
mediaType = "tv",
|
||||||
poster = buildImageUrl(credit.posterPath, "w500"),
|
language = language,
|
||||||
description = credit.overview?.takeIf { it.isNotBlank() },
|
)
|
||||||
releaseInfo = credit.firstAirDate?.take(4),
|
val poster = buildImageUrl(credit.posterPath, "w500")
|
||||||
rawReleaseDate = credit.firstAirDate,
|
?: buildImageUrl(credit.backdropPath, "w780")
|
||||||
popularity = credit.popularity,
|
?: artwork?.backdrop
|
||||||
)
|
?: return@async null
|
||||||
|
MetaPreview(
|
||||||
|
id = "tmdb:${credit.id}",
|
||||||
|
type = "series",
|
||||||
|
name = title,
|
||||||
|
poster = poster,
|
||||||
|
banner = buildImageUrl(credit.backdropPath, "w780") ?: artwork?.backdrop,
|
||||||
|
logo = artwork?.logo,
|
||||||
|
description = credit.overview?.takeIf { it.isNotBlank() },
|
||||||
|
releaseInfo = credit.firstAirDate?.take(4),
|
||||||
|
rawReleaseDate = credit.firstAirDate,
|
||||||
|
popularity = credit.popularity,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
.awaitAll()
|
||||||
|
.filterNotNull()
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun fetchEntityBrowse(
|
suspend fun fetchEntityBrowse(
|
||||||
|
|
@ -321,10 +394,16 @@ object TmdbMetadataService {
|
||||||
val results = response?.results.orEmpty()
|
val results = response?.results.orEmpty()
|
||||||
val totalPages = response?.totalPages ?: page
|
val totalPages = response?.totalPages ?: page
|
||||||
|
|
||||||
val mappedItems = results
|
val mappedItems = coroutineScope {
|
||||||
.filter { it.id > 0 }
|
results
|
||||||
.mapNotNull { item -> mapEntityDiscoverResult(item, mediaType) }
|
.filter { it.id > 0 }
|
||||||
.take(ENTITY_RAIL_MAX_ITEMS)
|
.map { item ->
|
||||||
|
async { mapEntityDiscoverResult(item, mediaType, language) }
|
||||||
|
}
|
||||||
|
.awaitAll()
|
||||||
|
.filterNotNull()
|
||||||
|
.take(ENTITY_RAIL_MAX_ITEMS)
|
||||||
|
}
|
||||||
|
|
||||||
TmdbEntityRailPageResult(
|
TmdbEntityRailPageResult(
|
||||||
items = mappedItems,
|
items = mappedItems,
|
||||||
|
|
@ -406,17 +485,26 @@ object TmdbMetadataService {
|
||||||
return header
|
return header
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun mapEntityDiscoverResult(
|
private suspend fun mapEntityDiscoverResult(
|
||||||
result: TmdbDiscoverResult,
|
result: TmdbDiscoverResult,
|
||||||
mediaType: TmdbEntityMediaType,
|
mediaType: TmdbEntityMediaType,
|
||||||
|
language: String,
|
||||||
): MetaPreview? {
|
): MetaPreview? {
|
||||||
val title = result.title?.takeIf { it.isNotBlank() }
|
val title = result.title?.takeIf { it.isNotBlank() }
|
||||||
?: result.name?.takeIf { it.isNotBlank() }
|
?: result.name?.takeIf { it.isNotBlank() }
|
||||||
?: result.originalTitle?.takeIf { it.isNotBlank() }
|
?: result.originalTitle?.takeIf { it.isNotBlank() }
|
||||||
?: result.originalName?.takeIf { it.isNotBlank() }
|
?: result.originalName?.takeIf { it.isNotBlank() }
|
||||||
?: return null
|
?: return null
|
||||||
|
|
||||||
|
val artwork = fetchPreviewArtwork(
|
||||||
|
tmdbId = result.id,
|
||||||
|
mediaType = mediaType.value,
|
||||||
|
language = language,
|
||||||
|
)
|
||||||
|
|
||||||
val poster = buildImageUrl(result.posterPath, "w500")
|
val poster = buildImageUrl(result.posterPath, "w500")
|
||||||
?: buildImageUrl(result.backdropPath, "w780")
|
?: buildImageUrl(result.backdropPath, "w780")
|
||||||
|
?: artwork?.backdrop
|
||||||
?: return null
|
?: return null
|
||||||
val releaseInfo = when (mediaType) {
|
val releaseInfo = when (mediaType) {
|
||||||
TmdbEntityMediaType.MOVIE -> result.releaseDate?.take(4)
|
TmdbEntityMediaType.MOVIE -> result.releaseDate?.take(4)
|
||||||
|
|
@ -427,11 +515,60 @@ object TmdbMetadataService {
|
||||||
type = if (mediaType == TmdbEntityMediaType.TV) "series" else "movie",
|
type = if (mediaType == TmdbEntityMediaType.TV) "series" else "movie",
|
||||||
name = title,
|
name = title,
|
||||||
poster = poster,
|
poster = poster,
|
||||||
|
banner = buildImageUrl(result.backdropPath, "w780") ?: artwork?.backdrop,
|
||||||
|
logo = artwork?.logo,
|
||||||
description = result.overview?.takeIf { it.isNotBlank() },
|
description = result.overview?.takeIf { it.isNotBlank() },
|
||||||
releaseInfo = releaseInfo,
|
releaseInfo = releaseInfo,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private data class TmdbPreviewArtwork(
|
||||||
|
val backdrop: String?,
|
||||||
|
val logo: String?,
|
||||||
|
)
|
||||||
|
|
||||||
|
private suspend fun fetchPreviewArtwork(
|
||||||
|
tmdbId: Int,
|
||||||
|
mediaType: String,
|
||||||
|
language: String,
|
||||||
|
): TmdbPreviewArtwork? = withContext(Dispatchers.Default) {
|
||||||
|
val normalizedLanguage = normalizeTmdbLanguage(language)
|
||||||
|
val cacheKey = "$tmdbId:$mediaType:$normalizedLanguage:preview_artwork"
|
||||||
|
previewArtworkCache[cacheKey]?.let { cached ->
|
||||||
|
return@withContext cached.takeIf { it.backdrop != null || it.logo != null }
|
||||||
|
}
|
||||||
|
|
||||||
|
val includeImageLanguage = buildString {
|
||||||
|
append(normalizedLanguage.substringBefore("-"))
|
||||||
|
append(",")
|
||||||
|
append(normalizedLanguage)
|
||||||
|
append(",en,null")
|
||||||
|
}
|
||||||
|
|
||||||
|
val response = coroutineScope {
|
||||||
|
val details = async {
|
||||||
|
fetch<TmdbDetailsResponse>(
|
||||||
|
endpoint = "$mediaType/$tmdbId",
|
||||||
|
query = mapOf("language" to normalizedLanguage),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
val images = async {
|
||||||
|
fetch<TmdbImagesResponse>(
|
||||||
|
endpoint = "$mediaType/$tmdbId/images",
|
||||||
|
query = mapOf("include_image_language" to includeImageLanguage),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
details.await() to images.await()
|
||||||
|
}
|
||||||
|
|
||||||
|
val artwork = TmdbPreviewArtwork(
|
||||||
|
backdrop = buildImageUrl(response.first?.backdropPath, "w1280"),
|
||||||
|
logo = buildImageUrl(response.second?.logos.orEmpty().selectBestLocalizedImagePath(normalizedLanguage), "w500"),
|
||||||
|
)
|
||||||
|
previewArtworkCache[cacheKey] = artwork
|
||||||
|
artwork.takeIf { it.backdrop != null || it.logo != null }
|
||||||
|
}
|
||||||
|
|
||||||
private fun buildEntityMediaOrder(
|
private fun buildEntityMediaOrder(
|
||||||
entityKind: TmdbEntityKind,
|
entityKind: TmdbEntityKind,
|
||||||
sourceType: String,
|
sourceType: String,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue