mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-05-17 15:32:01 +00:00
Merge branch 'postercard' into cmp-rewrite
This commit is contained in:
commit
a37c962b98
28 changed files with 1031 additions and 148 deletions
|
|
@ -32,6 +32,7 @@ import com.nuvio.app.features.settings.ThemeSettingsStorage
|
||||||
import com.nuvio.app.features.trakt.TraktAuthStorage
|
import com.nuvio.app.features.trakt.TraktAuthStorage
|
||||||
import com.nuvio.app.features.trakt.TraktCommentsStorage
|
import com.nuvio.app.features.trakt.TraktCommentsStorage
|
||||||
import com.nuvio.app.features.tmdb.TmdbSettingsStorage
|
import com.nuvio.app.features.tmdb.TmdbSettingsStorage
|
||||||
|
import com.nuvio.app.core.ui.PosterCardStyleStorage
|
||||||
import com.nuvio.app.features.watched.WatchedStorage
|
import com.nuvio.app.features.watched.WatchedStorage
|
||||||
import com.nuvio.app.features.streams.StreamLinkCacheStorage
|
import com.nuvio.app.features.streams.StreamLinkCacheStorage
|
||||||
import com.nuvio.app.features.watchprogress.ContinueWatchingEnrichmentStorage
|
import com.nuvio.app.features.watchprogress.ContinueWatchingEnrichmentStorage
|
||||||
|
|
@ -60,6 +61,7 @@ class MainActivity : ComponentActivity() {
|
||||||
SearchHistoryStorage.initialize(applicationContext)
|
SearchHistoryStorage.initialize(applicationContext)
|
||||||
SeasonViewModeStorage.initialize(applicationContext)
|
SeasonViewModeStorage.initialize(applicationContext)
|
||||||
ThemeSettingsStorage.initialize(applicationContext)
|
ThemeSettingsStorage.initialize(applicationContext)
|
||||||
|
PosterCardStyleStorage.initialize(applicationContext)
|
||||||
TmdbSettingsStorage.initialize(applicationContext)
|
TmdbSettingsStorage.initialize(applicationContext)
|
||||||
MdbListSettingsStorage.initialize(applicationContext)
|
MdbListSettingsStorage.initialize(applicationContext)
|
||||||
TraktAuthStorage.initialize(applicationContext)
|
TraktAuthStorage.initialize(applicationContext)
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ internal actual object PlatformLocalAccountDataCleaner {
|
||||||
"nuvio_player_settings",
|
"nuvio_player_settings",
|
||||||
"nuvio_profile_cache",
|
"nuvio_profile_cache",
|
||||||
"nuvio_theme_settings",
|
"nuvio_theme_settings",
|
||||||
|
"nuvio_poster_card_style",
|
||||||
"nuvio_mdblist_settings",
|
"nuvio_mdblist_settings",
|
||||||
"nuvio_trakt_auth",
|
"nuvio_trakt_auth",
|
||||||
"nuvio_watched",
|
"nuvio_watched",
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
package com.nuvio.app.core.ui
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.SharedPreferences
|
||||||
|
import com.nuvio.app.core.storage.ProfileScopedKey
|
||||||
|
|
||||||
|
actual object PosterCardStyleStorage {
|
||||||
|
private const val preferencesName = "nuvio_poster_card_style"
|
||||||
|
private const val payloadKey = "poster_card_style_payload"
|
||||||
|
|
||||||
|
private var preferences: SharedPreferences? = null
|
||||||
|
|
||||||
|
fun initialize(context: Context) {
|
||||||
|
preferences = context.getSharedPreferences(preferencesName, Context.MODE_PRIVATE)
|
||||||
|
}
|
||||||
|
|
||||||
|
actual fun loadPayload(): String? =
|
||||||
|
preferences?.getString(ProfileScopedKey.of(payloadKey), null)
|
||||||
|
|
||||||
|
actual fun savePayload(payload: String) {
|
||||||
|
preferences
|
||||||
|
?.edit()
|
||||||
|
?.putString(ProfileScopedKey.of(payloadKey), payload)
|
||||||
|
?.apply()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -20,6 +20,7 @@ import com.nuvio.app.features.settings.ThemeSettingsRepository
|
||||||
import com.nuvio.app.features.streams.StreamContextStore
|
import com.nuvio.app.features.streams.StreamContextStore
|
||||||
import com.nuvio.app.features.streams.StreamsRepository
|
import com.nuvio.app.features.streams.StreamsRepository
|
||||||
import com.nuvio.app.features.trakt.TraktAuthRepository
|
import com.nuvio.app.features.trakt.TraktAuthRepository
|
||||||
|
import com.nuvio.app.core.ui.PosterCardStyleRepository
|
||||||
import com.nuvio.app.features.watchprogress.ContinueWatchingPreferencesRepository
|
import com.nuvio.app.features.watchprogress.ContinueWatchingPreferencesRepository
|
||||||
import com.nuvio.app.features.watchprogress.WatchProgressRepository
|
import com.nuvio.app.features.watchprogress.WatchProgressRepository
|
||||||
import com.nuvio.app.features.watched.WatchedRepository
|
import com.nuvio.app.features.watched.WatchedRepository
|
||||||
|
|
@ -43,6 +44,7 @@ internal object LocalAccountDataCleaner {
|
||||||
EpisodeReleaseNotificationsRepository.clearLocalState()
|
EpisodeReleaseNotificationsRepository.clearLocalState()
|
||||||
CollectionRepository.clearLocalState()
|
CollectionRepository.clearLocalState()
|
||||||
ThemeSettingsRepository.clearLocalState()
|
ThemeSettingsRepository.clearLocalState()
|
||||||
|
PosterCardStyleRepository.clearLocalState()
|
||||||
TraktAuthRepository.clearLocalState()
|
TraktAuthRepository.clearLocalState()
|
||||||
PlayerSettingsRepository.clearLocalState()
|
PlayerSettingsRepository.clearLocalState()
|
||||||
CatalogRepository.clear()
|
CatalogRepository.clear()
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,8 @@ import com.nuvio.app.features.notifications.EpisodeReleaseNotificationsRepositor
|
||||||
import com.nuvio.app.features.player.PlayerSettingsStorage
|
import com.nuvio.app.features.player.PlayerSettingsStorage
|
||||||
import com.nuvio.app.features.player.PlayerSettingsRepository
|
import com.nuvio.app.features.player.PlayerSettingsRepository
|
||||||
import com.nuvio.app.features.profiles.ProfileRepository
|
import com.nuvio.app.features.profiles.ProfileRepository
|
||||||
|
import com.nuvio.app.core.ui.PosterCardStyleRepository
|
||||||
|
import com.nuvio.app.core.ui.PosterCardStyleStorage
|
||||||
import com.nuvio.app.features.settings.ThemeSettingsStorage
|
import com.nuvio.app.features.settings.ThemeSettingsStorage
|
||||||
import com.nuvio.app.features.settings.ThemeSettingsRepository
|
import com.nuvio.app.features.settings.ThemeSettingsRepository
|
||||||
import com.nuvio.app.features.tmdb.TmdbSettingsStorage
|
import com.nuvio.app.features.tmdb.TmdbSettingsStorage
|
||||||
|
|
@ -148,6 +150,7 @@ object ProfileSettingsSync {
|
||||||
val signatureFlows = listOf(
|
val signatureFlows = listOf(
|
||||||
ThemeSettingsRepository.selectedTheme.map { "theme" },
|
ThemeSettingsRepository.selectedTheme.map { "theme" },
|
||||||
ThemeSettingsRepository.amoledEnabled.map { "amoled" },
|
ThemeSettingsRepository.amoledEnabled.map { "amoled" },
|
||||||
|
PosterCardStyleRepository.uiState.map { "poster_card_style" },
|
||||||
PlayerSettingsRepository.uiState.map { "player" },
|
PlayerSettingsRepository.uiState.map { "player" },
|
||||||
TmdbSettingsRepository.uiState.map { "tmdb" },
|
TmdbSettingsRepository.uiState.map { "tmdb" },
|
||||||
MdbListSettingsRepository.uiState.map { "mdblist" },
|
MdbListSettingsRepository.uiState.map { "mdblist" },
|
||||||
|
|
@ -190,6 +193,7 @@ object ProfileSettingsSync {
|
||||||
return MobileProfileSettingsBlob(
|
return MobileProfileSettingsBlob(
|
||||||
features = MobileProfileSettingsFeatures(
|
features = MobileProfileSettingsFeatures(
|
||||||
themeSettings = ThemeSettingsStorage.exportToSyncPayload(),
|
themeSettings = ThemeSettingsStorage.exportToSyncPayload(),
|
||||||
|
posterCardStyleSettingsPayload = PosterCardStyleStorage.loadPayload().orEmpty().trim(),
|
||||||
playerSettings = PlayerSettingsStorage.exportToSyncPayload(),
|
playerSettings = PlayerSettingsStorage.exportToSyncPayload(),
|
||||||
tmdbSettings = TmdbSettingsStorage.exportToSyncPayload(),
|
tmdbSettings = TmdbSettingsStorage.exportToSyncPayload(),
|
||||||
mdbListSettings = MdbListSettingsStorage.exportToSyncPayload(),
|
mdbListSettings = MdbListSettingsStorage.exportToSyncPayload(),
|
||||||
|
|
@ -207,6 +211,9 @@ object ProfileSettingsSync {
|
||||||
ThemeSettingsStorage.replaceFromSyncPayload(blob.features.themeSettings)
|
ThemeSettingsStorage.replaceFromSyncPayload(blob.features.themeSettings)
|
||||||
ThemeSettingsRepository.onProfileChanged()
|
ThemeSettingsRepository.onProfileChanged()
|
||||||
|
|
||||||
|
PosterCardStyleStorage.savePayload(blob.features.posterCardStyleSettingsPayload)
|
||||||
|
PosterCardStyleRepository.onProfileChanged()
|
||||||
|
|
||||||
PlayerSettingsStorage.replaceFromSyncPayload(blob.features.playerSettings)
|
PlayerSettingsStorage.replaceFromSyncPayload(blob.features.playerSettings)
|
||||||
PlayerSettingsRepository.onProfileChanged()
|
PlayerSettingsRepository.onProfileChanged()
|
||||||
|
|
||||||
|
|
@ -231,6 +238,7 @@ object ProfileSettingsSync {
|
||||||
|
|
||||||
private fun ensureRepositoriesLoaded() {
|
private fun ensureRepositoriesLoaded() {
|
||||||
ThemeSettingsRepository.ensureLoaded()
|
ThemeSettingsRepository.ensureLoaded()
|
||||||
|
PosterCardStyleRepository.ensureLoaded()
|
||||||
PlayerSettingsRepository.ensureLoaded()
|
PlayerSettingsRepository.ensureLoaded()
|
||||||
TmdbSettingsRepository.ensureLoaded()
|
TmdbSettingsRepository.ensureLoaded()
|
||||||
MdbListSettingsRepository.ensureLoaded()
|
MdbListSettingsRepository.ensureLoaded()
|
||||||
|
|
@ -249,6 +257,7 @@ object ProfileSettingsSync {
|
||||||
private fun currentObservedStateSignature(): String = listOf(
|
private fun currentObservedStateSignature(): String = listOf(
|
||||||
"theme=${ThemeSettingsRepository.selectedTheme.value.name}",
|
"theme=${ThemeSettingsRepository.selectedTheme.value.name}",
|
||||||
"amoled=${ThemeSettingsRepository.amoledEnabled.value}",
|
"amoled=${ThemeSettingsRepository.amoledEnabled.value}",
|
||||||
|
"poster_card_style=${PosterCardStyleRepository.uiState.value}",
|
||||||
"player=${PlayerSettingsRepository.uiState.value}",
|
"player=${PlayerSettingsRepository.uiState.value}",
|
||||||
"tmdb=${TmdbSettingsRepository.uiState.value}",
|
"tmdb=${TmdbSettingsRepository.uiState.value}",
|
||||||
"mdblist=${MdbListSettingsRepository.uiState.value}",
|
"mdblist=${MdbListSettingsRepository.uiState.value}",
|
||||||
|
|
@ -268,6 +277,7 @@ private data class MobileProfileSettingsBlob(
|
||||||
@Serializable
|
@Serializable
|
||||||
private data class MobileProfileSettingsFeatures(
|
private data class MobileProfileSettingsFeatures(
|
||||||
@SerialName("theme_settings") val themeSettings: JsonObject = JsonObject(emptyMap()),
|
@SerialName("theme_settings") val themeSettings: JsonObject = JsonObject(emptyMap()),
|
||||||
|
@SerialName("poster_card_style_settings_payload") val posterCardStyleSettingsPayload: String = "",
|
||||||
@SerialName("player_settings") val playerSettings: JsonObject = JsonObject(emptyMap()),
|
@SerialName("player_settings") val playerSettings: JsonObject = JsonObject(emptyMap()),
|
||||||
@SerialName("tmdb_settings") val tmdbSettings: JsonObject = JsonObject(emptyMap()),
|
@SerialName("tmdb_settings") val tmdbSettings: JsonObject = JsonObject(emptyMap()),
|
||||||
@SerialName("mdblist_settings") val mdbListSettings: JsonObject = JsonObject(emptyMap()),
|
@SerialName("mdblist_settings") val mdbListSettings: JsonObject = JsonObject(emptyMap()),
|
||||||
|
|
|
||||||
|
|
@ -103,6 +103,8 @@ fun NuvioContinueWatchingActionSheet(
|
||||||
private fun ContinueWatchingSheetHeader(
|
private fun ContinueWatchingSheetHeader(
|
||||||
item: ContinueWatchingItem,
|
item: ContinueWatchingItem,
|
||||||
) {
|
) {
|
||||||
|
val posterCardStyle = rememberPosterCardStyleUiState()
|
||||||
|
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
|
|
@ -113,7 +115,7 @@ private fun ContinueWatchingSheetHeader(
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.size(width = 64.dp, height = 92.dp)
|
.size(width = 64.dp, height = 92.dp)
|
||||||
.clip(RoundedCornerShape(16.dp))
|
.clip(RoundedCornerShape(posterCardStyle.cornerRadiusDp.dp))
|
||||||
.background(MaterialTheme.colorScheme.surfaceVariant),
|
.background(MaterialTheme.colorScheme.surfaceVariant),
|
||||||
contentAlignment = Alignment.Center,
|
contentAlignment = Alignment.Center,
|
||||||
) {
|
) {
|
||||||
|
|
|
||||||
|
|
@ -140,6 +140,8 @@ fun NuvioAnimatedWatchedBadge(
|
||||||
private fun PosterSheetHeader(
|
private fun PosterSheetHeader(
|
||||||
item: MetaPreview,
|
item: MetaPreview,
|
||||||
) {
|
) {
|
||||||
|
val posterCardStyle = rememberPosterCardStyleUiState()
|
||||||
|
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
|
|
@ -150,7 +152,7 @@ private fun PosterSheetHeader(
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.size(width = 64.dp, height = 92.dp)
|
.size(width = 64.dp, height = 92.dp)
|
||||||
.clip(RoundedCornerShape(16.dp))
|
.clip(RoundedCornerShape(posterCardStyle.cornerRadiusDp.dp))
|
||||||
.background(MaterialTheme.colorScheme.surfaceVariant),
|
.background(MaterialTheme.colorScheme.surfaceVariant),
|
||||||
contentAlignment = Alignment.Center,
|
contentAlignment = Alignment.Center,
|
||||||
) {
|
) {
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.layout.widthIn
|
||||||
import androidx.compose.foundation.lazy.LazyRow
|
import androidx.compose.foundation.lazy.LazyRow
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
|
@ -98,19 +99,31 @@ fun NuvioPosterCard(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
shape: NuvioPosterShape = NuvioPosterShape.Poster,
|
shape: NuvioPosterShape = NuvioPosterShape.Poster,
|
||||||
detailLine: String? = null,
|
detailLine: String? = null,
|
||||||
|
showTitleBelow: Boolean = true,
|
||||||
|
bottomLeftLogoUrl: String? = null,
|
||||||
|
bottomLeftText: String? = null,
|
||||||
isWatched: Boolean = false,
|
isWatched: Boolean = false,
|
||||||
onClick: (() -> Unit)? = null,
|
onClick: (() -> Unit)? = null,
|
||||||
onLongClick: (() -> Unit)? = null,
|
onLongClick: (() -> Unit)? = null,
|
||||||
) {
|
) {
|
||||||
|
val posterCardStyle = rememberPosterCardStyleUiState()
|
||||||
|
val cardWidth = shape.cardWidth(basePosterWidthDp = posterCardStyle.widthDp)
|
||||||
|
val cardShape = RoundedCornerShape(posterCardStyle.cornerRadiusDp.dp)
|
||||||
|
val catalogLogoOverlaySize = catalogLogoOverlaySize(
|
||||||
|
basePosterWidthDp = posterCardStyle.widthDp,
|
||||||
|
shape = shape,
|
||||||
|
)
|
||||||
|
val shouldShowTitleBelow = showTitleBelow && !posterCardStyle.hideLabelsEnabled
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
modifier = modifier.width(shape.cardWidth),
|
modifier = modifier.width(cardWidth),
|
||||||
verticalArrangement = Arrangement.spacedBy(6.dp),
|
verticalArrangement = Arrangement.spacedBy(6.dp),
|
||||||
) {
|
) {
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.aspectRatio(shape.aspectRatio)
|
.aspectRatio(shape.aspectRatio)
|
||||||
.clip(RoundedCornerShape(16.dp))
|
.clip(cardShape)
|
||||||
.background(MaterialTheme.colorScheme.surface)
|
.background(MaterialTheme.colorScheme.surface)
|
||||||
.posterCardClickable(onClick = onClick, onLongClick = onLongClick),
|
.posterCardClickable(onClick = onClick, onLongClick = onLongClick),
|
||||||
contentAlignment = Alignment.Center,
|
contentAlignment = Alignment.Center,
|
||||||
|
|
@ -133,6 +146,35 @@ fun NuvioPosterCard(
|
||||||
overflow = TextOverflow.Ellipsis,
|
overflow = TextOverflow.Ellipsis,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!bottomLeftLogoUrl.isNullOrBlank() || !bottomLeftText.isNullOrBlank()) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.BottomStart)
|
||||||
|
.padding(horizontal = 10.dp, vertical = 10.dp),
|
||||||
|
) {
|
||||||
|
if (!bottomLeftLogoUrl.isNullOrBlank()) {
|
||||||
|
AsyncImage(
|
||||||
|
model = bottomLeftLogoUrl,
|
||||||
|
contentDescription = "$title logo",
|
||||||
|
modifier = Modifier
|
||||||
|
.width(catalogLogoOverlaySize.width)
|
||||||
|
.height(catalogLogoOverlaySize.height),
|
||||||
|
contentScale = ContentScale.Fit,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Text(
|
||||||
|
text = bottomLeftText.orEmpty(),
|
||||||
|
style = MaterialTheme.typography.labelMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onPrimary,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
modifier = Modifier.widthIn(max = catalogLogoOverlaySize.textMaxWidth),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
NuvioAnimatedWatchedBadge(
|
NuvioAnimatedWatchedBadge(
|
||||||
isVisible = isWatched,
|
isVisible = isWatched,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
|
@ -140,21 +182,25 @@ fun NuvioPosterCard(
|
||||||
.padding(6.dp),
|
.padding(6.dp),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
Text(
|
if (shouldShowTitleBelow) {
|
||||||
text = title,
|
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
|
||||||
color = MaterialTheme.colorScheme.onSurface,
|
|
||||||
maxLines = 1,
|
|
||||||
overflow = TextOverflow.Ellipsis,
|
|
||||||
)
|
|
||||||
if (!detailLine.isNullOrBlank()) {
|
|
||||||
Text(
|
Text(
|
||||||
text = detailLine,
|
text = title,
|
||||||
style = MaterialTheme.typography.labelSmall,
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
color = MaterialTheme.colorScheme.onSurface,
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
overflow = TextOverflow.Ellipsis,
|
overflow = TextOverflow.Ellipsis,
|
||||||
)
|
)
|
||||||
|
if (!detailLine.isNullOrBlank()) {
|
||||||
|
Text(
|
||||||
|
text = detailLine,
|
||||||
|
style = MaterialTheme.typography.labelSmall,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Box(modifier = Modifier.height(0.dp))
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
Box(modifier = Modifier.height(0.dp))
|
Box(modifier = Modifier.height(0.dp))
|
||||||
}
|
}
|
||||||
|
|
@ -251,14 +297,40 @@ private val NuvioPosterShape.aspectRatio: Float
|
||||||
get() = when (this) {
|
get() = when (this) {
|
||||||
NuvioPosterShape.Poster -> 0.675f
|
NuvioPosterShape.Poster -> 0.675f
|
||||||
NuvioPosterShape.Square -> 1f
|
NuvioPosterShape.Square -> 1f
|
||||||
NuvioPosterShape.Landscape -> 1.77f
|
NuvioPosterShape.Landscape -> PosterLandscapeAspectRatio
|
||||||
}
|
}
|
||||||
|
|
||||||
private val NuvioPosterShape.cardWidth: Dp
|
private data class CatalogLogoOverlaySize(
|
||||||
get() = when (this) {
|
val width: Dp,
|
||||||
NuvioPosterShape.Poster -> 110.dp
|
val height: Dp,
|
||||||
NuvioPosterShape.Square -> 110.dp
|
val textMaxWidth: Dp,
|
||||||
NuvioPosterShape.Landscape -> 180.dp
|
)
|
||||||
|
|
||||||
|
private fun catalogLogoOverlaySize(
|
||||||
|
basePosterWidthDp: Int,
|
||||||
|
shape: NuvioPosterShape,
|
||||||
|
): CatalogLogoOverlaySize =
|
||||||
|
if (shape == NuvioPosterShape.Landscape) {
|
||||||
|
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 =
|
||||||
|
when (this) {
|
||||||
|
NuvioPosterShape.Poster -> basePosterWidthDp.dp
|
||||||
|
NuvioPosterShape.Square -> basePosterWidthDp.dp
|
||||||
|
NuvioPosterShape.Landscape -> landscapePosterWidth(basePosterWidthDp)
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalFoundationApi::class)
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
package com.nuvio.app.core.ui
|
||||||
|
|
||||||
|
import androidx.compose.ui.unit.Dp
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
||||||
|
internal const val PosterLandscapeAspectRatio = 1.77f
|
||||||
|
private const val PosterLandscapeWidthScale = 180f / 110f
|
||||||
|
|
||||||
|
internal fun landscapePosterWidth(basePosterWidthDp: Int): Dp =
|
||||||
|
(basePosterWidthDp * PosterLandscapeWidthScale).dp
|
||||||
|
|
||||||
|
internal fun landscapePosterHeightForWidth(width: Dp): Dp =
|
||||||
|
(width.value / PosterLandscapeAspectRatio).dp
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
package com.nuvio.app.core.ui
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
internal fun rememberPosterCardStyleUiState(): PosterCardStyleUiState {
|
||||||
|
PosterCardStyleRepository.ensureLoaded()
|
||||||
|
val uiState by PosterCardStyleRepository.uiState.collectAsState()
|
||||||
|
return uiState
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,139 @@
|
||||||
|
package com.nuvio.app.core.ui
|
||||||
|
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlinx.serialization.decodeFromString
|
||||||
|
import kotlinx.serialization.encodeToString
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
|
||||||
|
internal const val DefaultPosterCardWidthDp = 126
|
||||||
|
internal const val DefaultPosterCardHeightDp = 189
|
||||||
|
internal const val DefaultPosterCardCornerRadiusDp = 12
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
private data class StoredPosterCardStylePreferences(
|
||||||
|
val widthDp: Int = DefaultPosterCardWidthDp,
|
||||||
|
val heightDp: Int = DefaultPosterCardHeightDp,
|
||||||
|
val cornerRadiusDp: Int = DefaultPosterCardCornerRadiusDp,
|
||||||
|
val catalogLandscapeModeEnabled: Boolean = false,
|
||||||
|
val hideLabelsEnabled: Boolean = false,
|
||||||
|
)
|
||||||
|
|
||||||
|
data class PosterCardStyleUiState(
|
||||||
|
val widthDp: Int = DefaultPosterCardWidthDp,
|
||||||
|
val heightDp: Int = DefaultPosterCardHeightDp,
|
||||||
|
val cornerRadiusDp: Int = DefaultPosterCardCornerRadiusDp,
|
||||||
|
val catalogLandscapeModeEnabled: Boolean = false,
|
||||||
|
val hideLabelsEnabled: Boolean = false,
|
||||||
|
)
|
||||||
|
|
||||||
|
object PosterCardStyleRepository {
|
||||||
|
private val json = Json {
|
||||||
|
ignoreUnknownKeys = true
|
||||||
|
encodeDefaults = true
|
||||||
|
}
|
||||||
|
|
||||||
|
private val _uiState = MutableStateFlow(PosterCardStyleUiState())
|
||||||
|
val uiState: StateFlow<PosterCardStyleUiState> = _uiState.asStateFlow()
|
||||||
|
|
||||||
|
private var hasLoaded = false
|
||||||
|
|
||||||
|
fun ensureLoaded() {
|
||||||
|
if (hasLoaded) return
|
||||||
|
loadFromDisk()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onProfileChanged() {
|
||||||
|
loadFromDisk()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun clearLocalState() {
|
||||||
|
hasLoaded = false
|
||||||
|
_uiState.value = PosterCardStyleUiState()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setWidthDp(widthDp: Int) {
|
||||||
|
ensureLoaded()
|
||||||
|
val nextWidth = widthDp
|
||||||
|
val nextHeight = (nextWidth * 3) / 2
|
||||||
|
if (_uiState.value.widthDp == nextWidth && _uiState.value.heightDp == nextHeight) return
|
||||||
|
_uiState.value = _uiState.value.copy(
|
||||||
|
widthDp = nextWidth,
|
||||||
|
heightDp = nextHeight,
|
||||||
|
)
|
||||||
|
persist()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setCornerRadiusDp(cornerRadiusDp: Int) {
|
||||||
|
ensureLoaded()
|
||||||
|
if (_uiState.value.cornerRadiusDp == cornerRadiusDp) return
|
||||||
|
_uiState.value = _uiState.value.copy(cornerRadiusDp = cornerRadiusDp)
|
||||||
|
persist()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setCatalogLandscapeModeEnabled(enabled: Boolean) {
|
||||||
|
ensureLoaded()
|
||||||
|
if (_uiState.value.catalogLandscapeModeEnabled == enabled) return
|
||||||
|
_uiState.value = _uiState.value.copy(catalogLandscapeModeEnabled = enabled)
|
||||||
|
persist()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setHideLabelsEnabled(enabled: Boolean) {
|
||||||
|
ensureLoaded()
|
||||||
|
if (_uiState.value.hideLabelsEnabled == enabled) return
|
||||||
|
_uiState.value = _uiState.value.copy(hideLabelsEnabled = enabled)
|
||||||
|
persist()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun resetToDefaults() {
|
||||||
|
ensureLoaded()
|
||||||
|
if (_uiState.value == PosterCardStyleUiState()) return
|
||||||
|
_uiState.value = PosterCardStyleUiState()
|
||||||
|
persist()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun loadFromDisk() {
|
||||||
|
hasLoaded = true
|
||||||
|
|
||||||
|
val payload = PosterCardStyleStorage.loadPayload().orEmpty().trim()
|
||||||
|
if (payload.isEmpty()) {
|
||||||
|
_uiState.value = PosterCardStyleUiState()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val stored = runCatching {
|
||||||
|
json.decodeFromString<StoredPosterCardStylePreferences>(payload)
|
||||||
|
}.getOrNull()
|
||||||
|
|
||||||
|
_uiState.value = if (stored != null) {
|
||||||
|
val widthDp = stored.widthDp.takeIf { it > 0 } ?: DefaultPosterCardWidthDp
|
||||||
|
val heightDp = stored.heightDp.takeIf { it > 0 } ?: ((widthDp * 3) / 2)
|
||||||
|
val cornerRadiusDp = stored.cornerRadiusDp.coerceAtLeast(0)
|
||||||
|
PosterCardStyleUiState(
|
||||||
|
widthDp = widthDp,
|
||||||
|
heightDp = heightDp,
|
||||||
|
cornerRadiusDp = cornerRadiusDp,
|
||||||
|
catalogLandscapeModeEnabled = stored.catalogLandscapeModeEnabled,
|
||||||
|
hideLabelsEnabled = stored.hideLabelsEnabled,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
PosterCardStyleUiState()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun persist() {
|
||||||
|
PosterCardStyleStorage.savePayload(
|
||||||
|
json.encodeToString(
|
||||||
|
StoredPosterCardStylePreferences(
|
||||||
|
widthDp = _uiState.value.widthDp,
|
||||||
|
heightDp = _uiState.value.heightDp,
|
||||||
|
cornerRadiusDp = _uiState.value.cornerRadiusDp,
|
||||||
|
catalogLandscapeModeEnabled = _uiState.value.catalogLandscapeModeEnabled,
|
||||||
|
hideLabelsEnabled = _uiState.value.hideLabelsEnabled,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
package com.nuvio.app.core.ui
|
||||||
|
|
||||||
|
internal expect object PosterCardStyleStorage {
|
||||||
|
fun loadPayload(): String?
|
||||||
|
fun savePayload(payload: String)
|
||||||
|
}
|
||||||
|
|
@ -47,6 +47,7 @@ import com.nuvio.app.core.ui.NuvioNetworkOfflineCard
|
||||||
import coil3.compose.AsyncImage
|
import coil3.compose.AsyncImage
|
||||||
import com.nuvio.app.core.format.formatReleaseDateForDisplay
|
import com.nuvio.app.core.format.formatReleaseDateForDisplay
|
||||||
import com.nuvio.app.core.ui.NuvioBackButton
|
import com.nuvio.app.core.ui.NuvioBackButton
|
||||||
|
import com.nuvio.app.core.ui.rememberPosterCardStyleUiState
|
||||||
import com.nuvio.app.core.ui.posterCardClickable
|
import com.nuvio.app.core.ui.posterCardClickable
|
||||||
import com.nuvio.app.core.ui.nuvioPlatformExtraBottomPadding
|
import com.nuvio.app.core.ui.nuvioPlatformExtraBottomPadding
|
||||||
import com.nuvio.app.features.home.MetaPreview
|
import com.nuvio.app.features.home.MetaPreview
|
||||||
|
|
@ -70,6 +71,7 @@ fun CatalogScreen(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
val uiState by CatalogRepository.uiState.collectAsStateWithLifecycle()
|
val uiState by CatalogRepository.uiState.collectAsStateWithLifecycle()
|
||||||
|
val posterCardStyle = rememberPosterCardStyleUiState()
|
||||||
val networkStatusUiState by NetworkStatusRepository.uiState.collectAsStateWithLifecycle()
|
val networkStatusUiState by NetworkStatusRepository.uiState.collectAsStateWithLifecycle()
|
||||||
val gridState = rememberLazyGridState()
|
val gridState = rememberLazyGridState()
|
||||||
var headerHeightPx by remember { mutableIntStateOf(0) }
|
var headerHeightPx by remember { mutableIntStateOf(0) }
|
||||||
|
|
@ -148,7 +150,9 @@ fun CatalogScreen(
|
||||||
verticalArrangement = Arrangement.spacedBy(18.dp),
|
verticalArrangement = Arrangement.spacedBy(18.dp),
|
||||||
) {
|
) {
|
||||||
if (uiState.items.isEmpty() && uiState.isLoading) {
|
if (uiState.items.isEmpty() && uiState.isLoading) {
|
||||||
items(columns * 3) { CatalogSkeletonTile() }
|
items(columns * 3) {
|
||||||
|
CatalogSkeletonTile(cornerRadiusDp = posterCardStyle.cornerRadiusDp)
|
||||||
|
}
|
||||||
} else if (uiState.items.isEmpty()) {
|
} else if (uiState.items.isEmpty()) {
|
||||||
item(span = { GridItemSpan(maxLineSpan) }) {
|
item(span = { GridItemSpan(maxLineSpan) }) {
|
||||||
CatalogEmptyState(
|
CatalogEmptyState(
|
||||||
|
|
@ -174,6 +178,8 @@ fun CatalogScreen(
|
||||||
) { item ->
|
) { item ->
|
||||||
CatalogPosterTile(
|
CatalogPosterTile(
|
||||||
item = item,
|
item = item,
|
||||||
|
cornerRadiusDp = posterCardStyle.cornerRadiusDp,
|
||||||
|
hideLabels = posterCardStyle.hideLabelsEnabled,
|
||||||
onClick = onPosterClick?.let { { it(item) } },
|
onClick = onPosterClick?.let { { it(item) } },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -242,6 +248,8 @@ private fun CatalogHeader(
|
||||||
@Composable
|
@Composable
|
||||||
private fun CatalogPosterTile(
|
private fun CatalogPosterTile(
|
||||||
item: MetaPreview,
|
item: MetaPreview,
|
||||||
|
cornerRadiusDp: Int,
|
||||||
|
hideLabels: Boolean,
|
||||||
onClick: (() -> Unit)? = null,
|
onClick: (() -> Unit)? = null,
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
|
|
@ -251,7 +259,7 @@ private fun CatalogPosterTile(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.aspectRatio(item.posterShape.catalogAspectRatio())
|
.aspectRatio(item.posterShape.catalogAspectRatio())
|
||||||
.clip(RoundedCornerShape(22.dp))
|
.clip(RoundedCornerShape(cornerRadiusDp.dp))
|
||||||
.background(MaterialTheme.colorScheme.surface)
|
.background(MaterialTheme.colorScheme.surface)
|
||||||
.posterCardClickable(onClick = onClick, onLongClick = null),
|
.posterCardClickable(onClick = onClick, onLongClick = null),
|
||||||
) {
|
) {
|
||||||
|
|
@ -264,35 +272,37 @@ 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))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun CatalogSkeletonTile() {
|
private fun CatalogSkeletonTile(cornerRadiusDp: Int) {
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.aspectRatio(0.68f)
|
.aspectRatio(0.68f)
|
||||||
.clip(RoundedCornerShape(22.dp))
|
.clip(RoundedCornerShape(cornerRadiusDp.dp))
|
||||||
.background(MaterialTheme.colorScheme.surface),
|
.background(MaterialTheme.colorScheme.surface),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -56,6 +56,9 @@ import androidx.compose.ui.unit.sp
|
||||||
import coil3.compose.AsyncImage
|
import coil3.compose.AsyncImage
|
||||||
import coil3.compose.LocalPlatformContext
|
import coil3.compose.LocalPlatformContext
|
||||||
import coil3.request.ImageRequest
|
import coil3.request.ImageRequest
|
||||||
|
import com.nuvio.app.core.ui.landscapePosterHeightForWidth
|
||||||
|
import com.nuvio.app.core.ui.landscapePosterWidth
|
||||||
|
import com.nuvio.app.core.ui.rememberPosterCardStyleUiState
|
||||||
import com.nuvio.app.features.details.components.DetailPosterRailSection
|
import com.nuvio.app.features.details.components.DetailPosterRailSection
|
||||||
import com.nuvio.app.features.home.MetaPreview
|
import com.nuvio.app.features.home.MetaPreview
|
||||||
import com.nuvio.app.features.tmdb.TmdbMetadataService
|
import com.nuvio.app.features.tmdb.TmdbMetadataService
|
||||||
|
|
@ -155,6 +158,18 @@ private fun PersonDetailContent(
|
||||||
sharedTransitionScope: SharedTransitionScope? = null,
|
sharedTransitionScope: SharedTransitionScope? = null,
|
||||||
animatedVisibilityScope: AnimatedVisibilityScope? = null,
|
animatedVisibilityScope: AnimatedVisibilityScope? = null,
|
||||||
) {
|
) {
|
||||||
|
val posterCardStyle = rememberPosterCardStyleUiState()
|
||||||
|
val isLandscapeShelfMode = posterCardStyle.catalogLandscapeModeEnabled
|
||||||
|
val skeletonPosterWidth = if (isLandscapeShelfMode) {
|
||||||
|
landscapePosterWidth(posterCardStyle.widthDp)
|
||||||
|
} else {
|
||||||
|
posterCardStyle.widthDp.dp
|
||||||
|
}
|
||||||
|
val skeletonPosterHeight = if (isLandscapeShelfMode) {
|
||||||
|
landscapePosterHeightForWidth(skeletonPosterWidth)
|
||||||
|
} else {
|
||||||
|
posterCardStyle.heightDp.dp
|
||||||
|
}
|
||||||
val accentColor = MaterialTheme.colorScheme.primary
|
val accentColor = MaterialTheme.colorScheme.primary
|
||||||
|
|
||||||
val allCredits = remember(person.movieCredits, person.tvCredits) {
|
val allCredits = remember(person.movieCredits, person.tvCredits) {
|
||||||
|
|
@ -445,6 +460,18 @@ private fun PersonDetailSkeleton(
|
||||||
sharedTransitionScope: SharedTransitionScope? = null,
|
sharedTransitionScope: SharedTransitionScope? = null,
|
||||||
animatedVisibilityScope: AnimatedVisibilityScope? = null,
|
animatedVisibilityScope: AnimatedVisibilityScope? = null,
|
||||||
) {
|
) {
|
||||||
|
val posterCardStyle = rememberPosterCardStyleUiState()
|
||||||
|
val isLandscapeShelfMode = posterCardStyle.catalogLandscapeModeEnabled
|
||||||
|
val skeletonPosterWidth = if (isLandscapeShelfMode) {
|
||||||
|
landscapePosterWidth(posterCardStyle.widthDp)
|
||||||
|
} else {
|
||||||
|
posterCardStyle.widthDp.dp
|
||||||
|
}
|
||||||
|
val skeletonPosterHeight = if (isLandscapeShelfMode) {
|
||||||
|
landscapePosterHeightForWidth(skeletonPosterWidth)
|
||||||
|
} else {
|
||||||
|
posterCardStyle.heightDp.dp
|
||||||
|
}
|
||||||
val accentColor = MaterialTheme.colorScheme.primary
|
val accentColor = MaterialTheme.colorScheme.primary
|
||||||
val avatarCacheKey = avatarTransitionKey
|
val avatarCacheKey = avatarTransitionKey
|
||||||
val platformContext = LocalPlatformContext.current
|
val platformContext = LocalPlatformContext.current
|
||||||
|
|
@ -599,24 +626,26 @@ private fun PersonDetailSkeleton(
|
||||||
horizontalArrangement = Arrangement.spacedBy(10.dp),
|
horizontalArrangement = Arrangement.spacedBy(10.dp),
|
||||||
) {
|
) {
|
||||||
repeat(4) {
|
repeat(4) {
|
||||||
Column(modifier = Modifier.width(110.dp)) {
|
Column(modifier = Modifier.width(skeletonPosterWidth)) {
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.width(110.dp)
|
.width(skeletonPosterWidth)
|
||||||
.height(163.dp)
|
.height(skeletonPosterHeight)
|
||||||
.clip(RoundedCornerShape(16.dp))
|
.clip(RoundedCornerShape(posterCardStyle.cornerRadiusDp.dp))
|
||||||
.background(MaterialTheme.colorScheme.surfaceVariant),
|
.background(MaterialTheme.colorScheme.surfaceVariant),
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.height(6.dp))
|
if (!isLandscapeShelfMode) {
|
||||||
SkeletonLine(
|
Spacer(modifier = Modifier.height(6.dp))
|
||||||
widthFraction = 1f,
|
SkeletonLine(
|
||||||
height = 16.dp,
|
widthFraction = 1f,
|
||||||
)
|
height = 16.dp,
|
||||||
Spacer(modifier = Modifier.height(4.dp))
|
)
|
||||||
SkeletonLine(
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
widthFraction = 0.56f,
|
SkeletonLine(
|
||||||
height = 12.dp,
|
widthFraction = 0.56f,
|
||||||
)
|
height = 12.dp,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -43,6 +43,9 @@ 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.unit.sp
|
||||||
import coil3.compose.AsyncImage
|
import coil3.compose.AsyncImage
|
||||||
|
import com.nuvio.app.core.ui.landscapePosterHeightForWidth
|
||||||
|
import com.nuvio.app.core.ui.landscapePosterWidth
|
||||||
|
import com.nuvio.app.core.ui.rememberPosterCardStyleUiState
|
||||||
import com.nuvio.app.features.details.components.DetailPosterRailSection
|
import com.nuvio.app.features.details.components.DetailPosterRailSection
|
||||||
import com.nuvio.app.features.home.MetaPreview
|
import com.nuvio.app.features.home.MetaPreview
|
||||||
import com.nuvio.app.features.tmdb.TmdbEntityBrowseData
|
import com.nuvio.app.features.tmdb.TmdbEntityBrowseData
|
||||||
|
|
@ -297,6 +300,19 @@ private fun EntityHeroSection(
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun EntityBrowseSkeleton() {
|
private fun EntityBrowseSkeleton() {
|
||||||
|
val posterCardStyle = rememberPosterCardStyleUiState()
|
||||||
|
val isLandscapeShelfMode = posterCardStyle.catalogLandscapeModeEnabled
|
||||||
|
val skeletonPosterWidth = if (isLandscapeShelfMode) {
|
||||||
|
landscapePosterWidth(posterCardStyle.widthDp)
|
||||||
|
} else {
|
||||||
|
posterCardStyle.widthDp.dp
|
||||||
|
}
|
||||||
|
val skeletonPosterHeight = if (isLandscapeShelfMode) {
|
||||||
|
landscapePosterHeightForWidth(skeletonPosterWidth)
|
||||||
|
} else {
|
||||||
|
posterCardStyle.heightDp.dp
|
||||||
|
}
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
|
|
@ -352,9 +368,9 @@ private fun EntityBrowseSkeleton() {
|
||||||
repeat(4) {
|
repeat(4) {
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.width(110.dp)
|
.width(skeletonPosterWidth)
|
||||||
.height(163.dp)
|
.height(skeletonPosterHeight)
|
||||||
.clip(RoundedCornerShape(16.dp))
|
.clip(RoundedCornerShape(posterCardStyle.cornerRadiusDp.dp))
|
||||||
.background(MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.3f)),
|
.background(MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.3f)),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.unit.Dp
|
import androidx.compose.ui.unit.Dp
|
||||||
import com.nuvio.app.core.ui.NuvioShelfSection
|
import com.nuvio.app.core.ui.NuvioShelfSection
|
||||||
import com.nuvio.app.core.ui.NuvioViewAllPillSize
|
import com.nuvio.app.core.ui.NuvioViewAllPillSize
|
||||||
|
import com.nuvio.app.core.ui.rememberPosterCardStyleUiState
|
||||||
import com.nuvio.app.features.home.HomeCatalogSection
|
import com.nuvio.app.features.home.HomeCatalogSection
|
||||||
import com.nuvio.app.features.home.MetaPreview
|
import com.nuvio.app.features.home.MetaPreview
|
||||||
import com.nuvio.app.features.home.stableKey
|
import com.nuvio.app.features.home.stableKey
|
||||||
|
|
@ -62,6 +63,8 @@ private fun HomeCatalogRowSectionContent(
|
||||||
onPosterClick: ((MetaPreview) -> Unit)?,
|
onPosterClick: ((MetaPreview) -> Unit)?,
|
||||||
onPosterLongClick: ((MetaPreview) -> Unit)?,
|
onPosterLongClick: ((MetaPreview) -> Unit)?,
|
||||||
) {
|
) {
|
||||||
|
val posterCardStyle = rememberPosterCardStyleUiState()
|
||||||
|
|
||||||
NuvioShelfSection(
|
NuvioShelfSection(
|
||||||
title = section.title,
|
title = section.title,
|
||||||
entries = entries,
|
entries = entries,
|
||||||
|
|
@ -74,6 +77,7 @@ private fun HomeCatalogRowSectionContent(
|
||||||
) { item ->
|
) { item ->
|
||||||
HomePosterCard(
|
HomePosterCard(
|
||||||
item = item,
|
item = item,
|
||||||
|
useLandscapeBackdropMode = posterCardStyle.catalogLandscapeModeEnabled,
|
||||||
isWatched = WatchingState.isPosterWatched(
|
isWatched = WatchingState.isPosterWatched(
|
||||||
watchedKeys = watchedKeys,
|
watchedKeys = watchedKeys,
|
||||||
item = item,
|
item = item,
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
package com.nuvio.app.features.home.components
|
package com.nuvio.app.features.home.components
|
||||||
|
|
||||||
import androidx.compose.foundation.clickable
|
|
||||||
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.BoxWithConstraints
|
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||||
|
|
@ -25,7 +24,10 @@ import androidx.compose.ui.unit.Dp
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import com.nuvio.app.core.ui.NuvioShelfSection
|
import com.nuvio.app.core.ui.NuvioShelfSection
|
||||||
|
import com.nuvio.app.core.ui.PosterLandscapeAspectRatio
|
||||||
|
import com.nuvio.app.core.ui.landscapePosterWidth
|
||||||
import com.nuvio.app.core.ui.posterCardClickable
|
import com.nuvio.app.core.ui.posterCardClickable
|
||||||
|
import com.nuvio.app.core.ui.rememberPosterCardStyleUiState
|
||||||
import com.nuvio.app.features.collection.Collection
|
import com.nuvio.app.features.collection.Collection
|
||||||
import com.nuvio.app.features.collection.CollectionFolder
|
import com.nuvio.app.features.collection.CollectionFolder
|
||||||
import com.nuvio.app.features.home.PosterShape
|
import com.nuvio.app.features.home.PosterShape
|
||||||
|
|
@ -86,21 +88,23 @@ private fun CollectionFolderCard(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
onClick: (() -> Unit)? = null,
|
onClick: (() -> Unit)? = null,
|
||||||
) {
|
) {
|
||||||
val shape = folder.posterShape
|
val posterCardStyle = rememberPosterCardStyleUiState()
|
||||||
|
val isLandscapeMode = posterCardStyle.catalogLandscapeModeEnabled
|
||||||
|
val shape = if (isLandscapeMode) PosterShape.Landscape else folder.posterShape
|
||||||
val cardWidth: Dp
|
val cardWidth: Dp
|
||||||
val aspectRatio: Float
|
val aspectRatio: Float
|
||||||
|
|
||||||
when (shape) {
|
when (shape) {
|
||||||
PosterShape.Poster -> {
|
PosterShape.Poster -> {
|
||||||
cardWidth = 110.dp
|
cardWidth = posterCardStyle.widthDp.dp
|
||||||
aspectRatio = 0.675f
|
aspectRatio = 0.675f
|
||||||
}
|
}
|
||||||
PosterShape.Landscape -> {
|
PosterShape.Landscape -> {
|
||||||
cardWidth = 180.dp
|
cardWidth = landscapePosterWidth(posterCardStyle.widthDp)
|
||||||
aspectRatio = 1.77f
|
aspectRatio = PosterLandscapeAspectRatio
|
||||||
}
|
}
|
||||||
PosterShape.Square -> {
|
PosterShape.Square -> {
|
||||||
cardWidth = 120.dp
|
cardWidth = posterCardStyle.widthDp.dp
|
||||||
aspectRatio = 1f
|
aspectRatio = 1f
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -109,7 +113,7 @@ private fun CollectionFolderCard(
|
||||||
modifier = modifier.width(cardWidth),
|
modifier = modifier.width(cardWidth),
|
||||||
verticalArrangement = Arrangement.spacedBy(6.dp),
|
verticalArrangement = Arrangement.spacedBy(6.dp),
|
||||||
) {
|
) {
|
||||||
val shapeCorner = RoundedCornerShape(16.dp)
|
val shapeCorner = RoundedCornerShape(posterCardStyle.cornerRadiusDp.dp)
|
||||||
val imageUrl = collectionFolderCardImageUrl(folder)
|
val imageUrl = collectionFolderCardImageUrl(folder)
|
||||||
Card(
|
Card(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import androidx.compose.ui.Modifier
|
||||||
import com.nuvio.app.core.format.formatReleaseDateForDisplay
|
import com.nuvio.app.core.format.formatReleaseDateForDisplay
|
||||||
import com.nuvio.app.core.ui.NuvioPosterCard
|
import com.nuvio.app.core.ui.NuvioPosterCard
|
||||||
import com.nuvio.app.core.ui.NuvioPosterShape
|
import com.nuvio.app.core.ui.NuvioPosterShape
|
||||||
|
import com.nuvio.app.core.ui.rememberPosterCardStyleUiState
|
||||||
import com.nuvio.app.features.home.MetaPreview
|
import com.nuvio.app.features.home.MetaPreview
|
||||||
import com.nuvio.app.features.home.PosterShape
|
import com.nuvio.app.features.home.PosterShape
|
||||||
|
|
||||||
|
|
@ -12,16 +13,23 @@ import com.nuvio.app.features.home.PosterShape
|
||||||
fun HomePosterCard(
|
fun HomePosterCard(
|
||||||
item: MetaPreview,
|
item: MetaPreview,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
|
useLandscapeBackdropMode: Boolean = false,
|
||||||
isWatched: Boolean = false,
|
isWatched: Boolean = false,
|
||||||
onClick: (() -> Unit)? = null,
|
onClick: (() -> Unit)? = null,
|
||||||
onLongClick: (() -> Unit)? = null,
|
onLongClick: (() -> Unit)? = null,
|
||||||
) {
|
) {
|
||||||
|
val posterCardStyle = rememberPosterCardStyleUiState()
|
||||||
|
val isLandscapeMode = useLandscapeBackdropMode || posterCardStyle.catalogLandscapeModeEnabled
|
||||||
|
|
||||||
NuvioPosterCard(
|
NuvioPosterCard(
|
||||||
title = item.name,
|
title = item.name,
|
||||||
imageUrl = item.poster,
|
imageUrl = if (isLandscapeMode) (item.banner ?: item.poster) else item.poster,
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
shape = item.posterShape.toNuvioPosterShape(),
|
shape = if (isLandscapeMode) NuvioPosterShape.Landscape else item.posterShape.toNuvioPosterShape(),
|
||||||
detailLine = item.releaseInfo?.let { formatReleaseDateForDisplay(it) },
|
detailLine = if (isLandscapeMode || posterCardStyle.hideLabelsEnabled) null else item.releaseInfo?.let { formatReleaseDateForDisplay(it) },
|
||||||
|
showTitleBelow = !posterCardStyle.hideLabelsEnabled,
|
||||||
|
bottomLeftLogoUrl = if (isLandscapeMode) item.logo 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,
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,9 @@ import androidx.compose.ui.geometry.Offset
|
||||||
import androidx.compose.ui.graphics.Brush
|
import androidx.compose.ui.graphics.Brush
|
||||||
import androidx.compose.ui.unit.Dp
|
import androidx.compose.ui.unit.Dp
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.nuvio.app.core.ui.landscapePosterHeightForWidth
|
||||||
|
import com.nuvio.app.core.ui.landscapePosterWidth
|
||||||
|
import com.nuvio.app.core.ui.rememberPosterCardStyleUiState
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun rememberHomeSkeletonBrush(): Brush {
|
private fun rememberHomeSkeletonBrush(): Brush {
|
||||||
|
|
@ -180,6 +183,17 @@ fun HomeSkeletonHero(
|
||||||
@Composable
|
@Composable
|
||||||
fun HomeSkeletonRow(modifier: Modifier = Modifier) {
|
fun HomeSkeletonRow(modifier: Modifier = Modifier) {
|
||||||
val brush = rememberHomeSkeletonBrush()
|
val brush = rememberHomeSkeletonBrush()
|
||||||
|
val posterCardStyle = rememberPosterCardStyleUiState()
|
||||||
|
val skeletonWidth = if (posterCardStyle.catalogLandscapeModeEnabled) {
|
||||||
|
landscapePosterWidth(posterCardStyle.widthDp)
|
||||||
|
} else {
|
||||||
|
posterCardStyle.widthDp.dp
|
||||||
|
}
|
||||||
|
val skeletonHeight = if (posterCardStyle.catalogLandscapeModeEnabled) {
|
||||||
|
landscapePosterHeightForWidth(skeletonWidth)
|
||||||
|
} else {
|
||||||
|
posterCardStyle.heightDp.dp
|
||||||
|
}
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
modifier = modifier.fillMaxWidth(),
|
modifier = modifier.fillMaxWidth(),
|
||||||
|
|
@ -209,9 +223,9 @@ fun HomeSkeletonRow(modifier: Modifier = Modifier) {
|
||||||
repeat(4) {
|
repeat(4) {
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.width(110.dp)
|
.width(skeletonWidth)
|
||||||
.height(163.dp)
|
.height(skeletonHeight)
|
||||||
.clip(RoundedCornerShape(16.dp))
|
.clip(RoundedCornerShape(posterCardStyle.cornerRadiusDp.dp))
|
||||||
.background(brush),
|
.background(brush),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ import com.nuvio.app.features.collection.CollectionRepository
|
||||||
import com.nuvio.app.features.downloads.DownloadsRepository
|
import com.nuvio.app.features.downloads.DownloadsRepository
|
||||||
import com.nuvio.app.features.details.MetaScreenSettingsRepository
|
import com.nuvio.app.features.details.MetaScreenSettingsRepository
|
||||||
import com.nuvio.app.features.home.HomeCatalogSettingsRepository
|
import com.nuvio.app.features.home.HomeCatalogSettingsRepository
|
||||||
|
import com.nuvio.app.core.ui.PosterCardStyleRepository
|
||||||
import com.nuvio.app.features.library.LibraryRepository
|
import com.nuvio.app.features.library.LibraryRepository
|
||||||
import com.nuvio.app.features.mdblist.MdbListSettingsRepository
|
import com.nuvio.app.features.mdblist.MdbListSettingsRepository
|
||||||
import com.nuvio.app.features.notifications.EpisodeReleaseNotificationsRepository
|
import com.nuvio.app.features.notifications.EpisodeReleaseNotificationsRepository
|
||||||
|
|
@ -138,6 +139,7 @@ object ProfileRepository {
|
||||||
PluginRepository.onProfileChanged(profileIndex)
|
PluginRepository.onProfileChanged(profileIndex)
|
||||||
}
|
}
|
||||||
ThemeSettingsRepository.onProfileChanged()
|
ThemeSettingsRepository.onProfileChanged()
|
||||||
|
PosterCardStyleRepository.onProfileChanged()
|
||||||
PlayerSettingsRepository.onProfileChanged()
|
PlayerSettingsRepository.onProfileChanged()
|
||||||
HomeCatalogSettingsRepository.onProfileChanged()
|
HomeCatalogSettingsRepository.onProfileChanged()
|
||||||
MetaScreenSettingsRepository.onProfileChanged()
|
MetaScreenSettingsRepository.onProfileChanged()
|
||||||
|
|
|
||||||
|
|
@ -56,6 +56,7 @@ import com.nuvio.app.core.ui.NuvioBottomSheetDivider
|
||||||
import com.nuvio.app.core.ui.NuvioModalBottomSheet
|
import com.nuvio.app.core.ui.NuvioModalBottomSheet
|
||||||
import com.nuvio.app.core.ui.dismissNuvioBottomSheet
|
import com.nuvio.app.core.ui.dismissNuvioBottomSheet
|
||||||
import com.nuvio.app.core.ui.nuvioPlatformExtraBottomPadding
|
import com.nuvio.app.core.ui.nuvioPlatformExtraBottomPadding
|
||||||
|
import com.nuvio.app.core.ui.rememberPosterCardStyleUiState
|
||||||
import com.nuvio.app.core.ui.posterCardClickable
|
import com.nuvio.app.core.ui.posterCardClickable
|
||||||
import com.nuvio.app.features.home.MetaPreview
|
import com.nuvio.app.features.home.MetaPreview
|
||||||
import com.nuvio.app.features.home.PosterShape
|
import com.nuvio.app.features.home.PosterShape
|
||||||
|
|
@ -338,6 +339,8 @@ private fun DiscoverGridRow(
|
||||||
onPosterClick: ((MetaPreview) -> Unit)? = null,
|
onPosterClick: ((MetaPreview) -> Unit)? = null,
|
||||||
onPosterLongClick: ((MetaPreview) -> Unit)? = null,
|
onPosterLongClick: ((MetaPreview) -> Unit)? = null,
|
||||||
) {
|
) {
|
||||||
|
val posterCardStyle = rememberPosterCardStyleUiState()
|
||||||
|
|
||||||
Row(
|
Row(
|
||||||
modifier = modifier.fillMaxWidth(),
|
modifier = modifier.fillMaxWidth(),
|
||||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||||
|
|
@ -346,6 +349,8 @@ private fun DiscoverGridRow(
|
||||||
items.forEach { item ->
|
items.forEach { item ->
|
||||||
DiscoverPosterTile(
|
DiscoverPosterTile(
|
||||||
item = item,
|
item = item,
|
||||||
|
cornerRadiusDp = posterCardStyle.cornerRadiusDp,
|
||||||
|
hideLabels = posterCardStyle.hideLabelsEnabled,
|
||||||
modifier = Modifier.weight(1f),
|
modifier = Modifier.weight(1f),
|
||||||
isWatched = WatchingState.isPosterWatched(
|
isWatched = WatchingState.isPosterWatched(
|
||||||
watchedKeys = watchedKeys,
|
watchedKeys = watchedKeys,
|
||||||
|
|
@ -365,6 +370,8 @@ private fun DiscoverGridRow(
|
||||||
@Composable
|
@Composable
|
||||||
private fun DiscoverPosterTile(
|
private fun DiscoverPosterTile(
|
||||||
item: MetaPreview,
|
item: MetaPreview,
|
||||||
|
cornerRadiusDp: Int,
|
||||||
|
hideLabels: Boolean,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
isWatched: Boolean = false,
|
isWatched: Boolean = false,
|
||||||
onClick: (() -> Unit)? = null,
|
onClick: (() -> Unit)? = null,
|
||||||
|
|
@ -378,7 +385,7 @@ private fun DiscoverPosterTile(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.aspectRatio(item.posterShape.discoverAspectRatio())
|
.aspectRatio(item.posterShape.discoverAspectRatio())
|
||||||
.clip(RoundedCornerShape(22.dp))
|
.clip(RoundedCornerShape(cornerRadiusDp.dp))
|
||||||
.background(MaterialTheme.colorScheme.surface)
|
.background(MaterialTheme.colorScheme.surface)
|
||||||
.posterCardClickable(onClick = onClick, onLongClick = onLongClick),
|
.posterCardClickable(onClick = onClick, onLongClick = onLongClick),
|
||||||
) {
|
) {
|
||||||
|
|
@ -397,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))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -424,6 +433,8 @@ private fun DiscoverSkeletonRow(
|
||||||
columns: Int,
|
columns: Int,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
|
val posterCardStyle = rememberPosterCardStyleUiState()
|
||||||
|
|
||||||
Row(
|
Row(
|
||||||
modifier = modifier.fillMaxWidth(),
|
modifier = modifier.fillMaxWidth(),
|
||||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||||
|
|
@ -433,7 +444,7 @@ private fun DiscoverSkeletonRow(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.weight(1f)
|
.weight(1f)
|
||||||
.aspectRatio(0.68f)
|
.aspectRatio(0.68f)
|
||||||
.clip(RoundedCornerShape(22.dp))
|
.clip(RoundedCornerShape(posterCardStyle.cornerRadiusDp.dp))
|
||||||
.background(MaterialTheme.colorScheme.surface),
|
.background(MaterialTheme.colorScheme.surface),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.Check
|
import androidx.compose.material.icons.filled.Check
|
||||||
import androidx.compose.material.icons.rounded.Style
|
import androidx.compose.material.icons.rounded.Style
|
||||||
|
import androidx.compose.material.icons.rounded.Tune
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
|
|
@ -41,6 +42,7 @@ internal fun LazyListScope.appearanceSettingsContent(
|
||||||
amoledEnabled: Boolean,
|
amoledEnabled: Boolean,
|
||||||
onAmoledToggle: (Boolean) -> Unit,
|
onAmoledToggle: (Boolean) -> Unit,
|
||||||
onContinueWatchingClick: () -> Unit,
|
onContinueWatchingClick: () -> Unit,
|
||||||
|
onPosterCustomizationClick: () -> Unit,
|
||||||
) {
|
) {
|
||||||
item {
|
item {
|
||||||
SettingsSection(
|
SettingsSection(
|
||||||
|
|
@ -101,6 +103,14 @@ internal fun LazyListScope.appearanceSettingsContent(
|
||||||
isTablet = isTablet,
|
isTablet = isTablet,
|
||||||
onClick = onContinueWatchingClick,
|
onClick = onContinueWatchingClick,
|
||||||
)
|
)
|
||||||
|
SettingsGroupDivider(isTablet = isTablet)
|
||||||
|
SettingsNavigationRow(
|
||||||
|
title = "Poster Customization",
|
||||||
|
description = "Adjust shared poster card width and corner radius presets.",
|
||||||
|
icon = Icons.Rounded.Tune,
|
||||||
|
isTablet = isTablet,
|
||||||
|
onClick = onPosterCustomizationClick,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,310 @@
|
||||||
|
package com.nuvio.app.features.settings
|
||||||
|
|
||||||
|
import androidx.compose.animation.core.animateDpAsState
|
||||||
|
import androidx.compose.animation.core.tween
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.border
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||||
|
import androidx.compose.foundation.layout.FlowRow
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.lazy.LazyListScope
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material3.FilterChip
|
||||||
|
import androidx.compose.material3.FilterChipDefaults
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Switch
|
||||||
|
import androidx.compose.material3.SwitchDefaults
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.nuvio.app.core.ui.NuvioActionLabel
|
||||||
|
import com.nuvio.app.core.ui.PosterCardStyleRepository
|
||||||
|
import com.nuvio.app.core.ui.PosterCardStyleUiState
|
||||||
|
|
||||||
|
internal fun LazyListScope.posterCustomizationSettingsContent(
|
||||||
|
isTablet: Boolean,
|
||||||
|
uiState: PosterCardStyleUiState,
|
||||||
|
) {
|
||||||
|
item {
|
||||||
|
SettingsSection(
|
||||||
|
title = "POSTER CARD STYLE",
|
||||||
|
isTablet = isTablet,
|
||||||
|
actions = {
|
||||||
|
NuvioActionLabel(
|
||||||
|
text = "Reset",
|
||||||
|
onClick = PosterCardStyleRepository::resetToDefaults,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
SettingsGroup(isTablet = isTablet) {
|
||||||
|
PosterCardStyleControls(
|
||||||
|
isTablet = isTablet,
|
||||||
|
widthDp = uiState.widthDp,
|
||||||
|
cornerRadiusDp = uiState.cornerRadiusDp,
|
||||||
|
catalogLandscapeModeEnabled = uiState.catalogLandscapeModeEnabled,
|
||||||
|
hideLabelsEnabled = uiState.hideLabelsEnabled,
|
||||||
|
onWidthSelected = PosterCardStyleRepository::setWidthDp,
|
||||||
|
onCornerRadiusSelected = PosterCardStyleRepository::setCornerRadiusDp,
|
||||||
|
onCatalogLandscapeModeChange = PosterCardStyleRepository::setCatalogLandscapeModeEnabled,
|
||||||
|
onHideLabelsChange = PosterCardStyleRepository::setHideLabelsEnabled,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalLayoutApi::class)
|
||||||
|
@Composable
|
||||||
|
private fun PosterCardStyleControls(
|
||||||
|
isTablet: Boolean,
|
||||||
|
widthDp: Int,
|
||||||
|
cornerRadiusDp: Int,
|
||||||
|
catalogLandscapeModeEnabled: Boolean,
|
||||||
|
hideLabelsEnabled: Boolean,
|
||||||
|
onWidthSelected: (Int) -> Unit,
|
||||||
|
onCornerRadiusSelected: (Int) -> Unit,
|
||||||
|
onCatalogLandscapeModeChange: (Boolean) -> Unit,
|
||||||
|
onHideLabelsChange: (Boolean) -> Unit,
|
||||||
|
) {
|
||||||
|
val widthOptions = listOf(
|
||||||
|
PresetOption("Compact", 104),
|
||||||
|
PresetOption("Dense", 112),
|
||||||
|
PresetOption("Standard", 120),
|
||||||
|
PresetOption("Balanced", 126),
|
||||||
|
PresetOption("Comfort", 134),
|
||||||
|
PresetOption("Large", 140),
|
||||||
|
)
|
||||||
|
val radiusOptions = listOf(
|
||||||
|
PresetOption("Sharp", 0),
|
||||||
|
PresetOption("Subtle", 4),
|
||||||
|
PresetOption("Classic", 8),
|
||||||
|
PresetOption("Rounded", 12),
|
||||||
|
PresetOption("Pill", 16),
|
||||||
|
)
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = if (isTablet) 20.dp else 16.dp, vertical = if (isTablet) 18.dp else 16.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(14.dp),
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "Customize card width and corner radius for shared poster cards across the app.",
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
)
|
||||||
|
PosterCardLivePreview(
|
||||||
|
widthDp = widthDp,
|
||||||
|
cornerRadiusDp = cornerRadiusDp,
|
||||||
|
)
|
||||||
|
PosterStyleOptionRow(
|
||||||
|
title = "Card Width",
|
||||||
|
selectedValue = widthDp,
|
||||||
|
options = widthOptions,
|
||||||
|
onSelected = onWidthSelected,
|
||||||
|
)
|
||||||
|
PosterStyleOptionRow(
|
||||||
|
title = "Card Radius",
|
||||||
|
selectedValue = cornerRadiusDp,
|
||||||
|
options = radiusOptions,
|
||||||
|
onSelected = onCornerRadiusSelected,
|
||||||
|
)
|
||||||
|
PosterLandscapeModeToggleRow(
|
||||||
|
checked = catalogLandscapeModeEnabled,
|
||||||
|
onCheckedChange = onCatalogLandscapeModeChange,
|
||||||
|
)
|
||||||
|
PosterToggleRow(
|
||||||
|
title = "Hide labels",
|
||||||
|
checked = hideLabelsEnabled,
|
||||||
|
onCheckedChange = onHideLabelsChange,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun PosterLandscapeModeToggleRow(
|
||||||
|
checked: Boolean,
|
||||||
|
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(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = title,
|
||||||
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
|
color = MaterialTheme.colorScheme.onSurface,
|
||||||
|
fontWeight = FontWeight.Medium,
|
||||||
|
)
|
||||||
|
Switch(
|
||||||
|
checked = checked,
|
||||||
|
onCheckedChange = onCheckedChange,
|
||||||
|
colors = SwitchDefaults.colors(
|
||||||
|
checkedThumbColor = MaterialTheme.colorScheme.onPrimary,
|
||||||
|
checkedTrackColor = MaterialTheme.colorScheme.primary,
|
||||||
|
uncheckedThumbColor = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
uncheckedTrackColor = MaterialTheme.colorScheme.outlineVariant,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun PosterCardLivePreview(
|
||||||
|
widthDp: Int,
|
||||||
|
cornerRadiusDp: Int,
|
||||||
|
) {
|
||||||
|
val targetHeightDp = (widthDp * 3) / 2
|
||||||
|
val previewFrameWidthDp = 140
|
||||||
|
val previewFrameHeightDp = 210
|
||||||
|
val animatedWidth = animateDpAsState(
|
||||||
|
targetValue = widthDp.dp,
|
||||||
|
animationSpec = tween(durationMillis = 280),
|
||||||
|
label = "posterPreviewWidth",
|
||||||
|
)
|
||||||
|
val animatedHeight = animateDpAsState(
|
||||||
|
targetValue = targetHeightDp.dp,
|
||||||
|
animationSpec = tween(durationMillis = 280),
|
||||||
|
label = "posterPreviewHeight",
|
||||||
|
)
|
||||||
|
val animatedCornerRadius = animateDpAsState(
|
||||||
|
targetValue = cornerRadiusDp.dp,
|
||||||
|
animationSpec = tween(durationMillis = 220),
|
||||||
|
label = "posterPreviewCornerRadius",
|
||||||
|
)
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "Live Preview",
|
||||||
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
|
color = MaterialTheme.colorScheme.onSurface,
|
||||||
|
fontWeight = FontWeight.SemiBold,
|
||||||
|
)
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(14.dp),
|
||||||
|
verticalAlignment = Alignment.Top,
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.width(previewFrameWidthDp.dp)
|
||||||
|
.height(previewFrameHeightDp.dp),
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.width(animatedWidth.value)
|
||||||
|
.height(animatedHeight.value)
|
||||||
|
.clip(RoundedCornerShape(animatedCornerRadius.value))
|
||||||
|
.background(MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.45f))
|
||||||
|
.border(
|
||||||
|
width = 1.dp,
|
||||||
|
color = MaterialTheme.colorScheme.outlineVariant,
|
||||||
|
shape = RoundedCornerShape(animatedCornerRadius.value),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Column(
|
||||||
|
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "Width: ${widthDp}dp",
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurface,
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = "Corner radius: ${cornerRadiusDp}dp",
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurface,
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = "Height: ${targetHeightDp}dp",
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(1.dp)
|
||||||
|
.background(MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.5f)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalLayoutApi::class)
|
||||||
|
@Composable
|
||||||
|
private fun PosterStyleOptionRow(
|
||||||
|
title: String,
|
||||||
|
selectedValue: Int,
|
||||||
|
options: List<PresetOption>,
|
||||||
|
onSelected: (Int) -> Unit,
|
||||||
|
) {
|
||||||
|
val selectedLabel = options.firstOrNull { it.value == selectedValue }?.label ?: "Custom"
|
||||||
|
|
||||||
|
Column(
|
||||||
|
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "$title ($selectedLabel)",
|
||||||
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
|
color = MaterialTheme.colorScheme.onSurface,
|
||||||
|
fontWeight = FontWeight.SemiBold,
|
||||||
|
)
|
||||||
|
|
||||||
|
FlowRow(
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
) {
|
||||||
|
options.forEach { option ->
|
||||||
|
FilterChip(
|
||||||
|
selected = option.value == selectedValue,
|
||||||
|
onClick = { onSelected(option.value) },
|
||||||
|
label = { Text(option.label) },
|
||||||
|
colors = FilterChipDefaults.filterChipColors(
|
||||||
|
selectedContainerColor = MaterialTheme.colorScheme.primaryContainer,
|
||||||
|
selectedLabelColor = MaterialTheme.colorScheme.onPrimaryContainer,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private data class PresetOption(
|
||||||
|
val label: String,
|
||||||
|
val value: Int,
|
||||||
|
)
|
||||||
|
|
@ -49,6 +49,11 @@ internal enum class SettingsPage(
|
||||||
category = SettingsCategory.General,
|
category = SettingsCategory.General,
|
||||||
parentPage = Appearance,
|
parentPage = Appearance,
|
||||||
),
|
),
|
||||||
|
PosterCustomization(
|
||||||
|
title = "Poster Customization",
|
||||||
|
category = SettingsCategory.General,
|
||||||
|
parentPage = Appearance,
|
||||||
|
),
|
||||||
ContentDiscovery(
|
ContentDiscovery(
|
||||||
title = "Content & Discovery",
|
title = "Content & Discovery",
|
||||||
category = SettingsCategory.General,
|
category = SettingsCategory.General,
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,8 @@ import com.nuvio.app.core.ui.PlatformBackHandler
|
||||||
import com.nuvio.app.features.addons.AddonRepository
|
import com.nuvio.app.features.addons.AddonRepository
|
||||||
import com.nuvio.app.features.details.MetaScreenSettingsRepository
|
import com.nuvio.app.features.details.MetaScreenSettingsRepository
|
||||||
import com.nuvio.app.features.details.MetaScreenSettingsUiState
|
import com.nuvio.app.features.details.MetaScreenSettingsUiState
|
||||||
|
import com.nuvio.app.core.ui.PosterCardStyleRepository
|
||||||
|
import com.nuvio.app.core.ui.PosterCardStyleUiState
|
||||||
import com.nuvio.app.features.home.HomeCatalogSettingsItem
|
import com.nuvio.app.features.home.HomeCatalogSettingsItem
|
||||||
import com.nuvio.app.features.home.HomeCatalogSettingsRepository
|
import com.nuvio.app.features.home.HomeCatalogSettingsRepository
|
||||||
import com.nuvio.app.features.mdblist.MdbListSettings
|
import com.nuvio.app.features.mdblist.MdbListSettings
|
||||||
|
|
@ -129,6 +131,10 @@ fun SettingsScreen(
|
||||||
ContinueWatchingPreferencesRepository.ensureLoaded()
|
ContinueWatchingPreferencesRepository.ensureLoaded()
|
||||||
ContinueWatchingPreferencesRepository.uiState
|
ContinueWatchingPreferencesRepository.uiState
|
||||||
}.collectAsStateWithLifecycle()
|
}.collectAsStateWithLifecycle()
|
||||||
|
val posterCardStyleUiState by remember {
|
||||||
|
PosterCardStyleRepository.ensureLoaded()
|
||||||
|
PosterCardStyleRepository.uiState
|
||||||
|
}.collectAsStateWithLifecycle()
|
||||||
val episodeReleaseNotificationsUiState by remember {
|
val episodeReleaseNotificationsUiState by remember {
|
||||||
EpisodeReleaseNotificationsRepository.ensureLoaded()
|
EpisodeReleaseNotificationsRepository.ensureLoaded()
|
||||||
EpisodeReleaseNotificationsRepository.uiState
|
EpisodeReleaseNotificationsRepository.uiState
|
||||||
|
|
@ -179,6 +185,7 @@ fun SettingsScreen(
|
||||||
homescreenItems = homescreenSettingsUiState.items,
|
homescreenItems = homescreenSettingsUiState.items,
|
||||||
metaScreenSettingsUiState = metaScreenSettingsUiState,
|
metaScreenSettingsUiState = metaScreenSettingsUiState,
|
||||||
continueWatchingPreferencesUiState = continueWatchingPreferencesUiState,
|
continueWatchingPreferencesUiState = continueWatchingPreferencesUiState,
|
||||||
|
posterCardStyleUiState = posterCardStyleUiState,
|
||||||
onSwitchProfile = onSwitchProfile,
|
onSwitchProfile = onSwitchProfile,
|
||||||
onDownloadsClick = onDownloadsClick,
|
onDownloadsClick = onDownloadsClick,
|
||||||
onCollectionsClick = onCollectionsClick,
|
onCollectionsClick = onCollectionsClick,
|
||||||
|
|
@ -214,6 +221,7 @@ fun SettingsScreen(
|
||||||
homescreenItems = homescreenSettingsUiState.items,
|
homescreenItems = homescreenSettingsUiState.items,
|
||||||
metaScreenSettingsUiState = metaScreenSettingsUiState,
|
metaScreenSettingsUiState = metaScreenSettingsUiState,
|
||||||
continueWatchingPreferencesUiState = continueWatchingPreferencesUiState,
|
continueWatchingPreferencesUiState = continueWatchingPreferencesUiState,
|
||||||
|
posterCardStyleUiState = posterCardStyleUiState,
|
||||||
onSwitchProfile = onSwitchProfile,
|
onSwitchProfile = onSwitchProfile,
|
||||||
onHomescreenClick = onHomescreenClick,
|
onHomescreenClick = onHomescreenClick,
|
||||||
onMetaScreenClick = onMetaScreenClick,
|
onMetaScreenClick = onMetaScreenClick,
|
||||||
|
|
@ -259,6 +267,7 @@ private fun MobileSettingsScreen(
|
||||||
homescreenItems: List<HomeCatalogSettingsItem>,
|
homescreenItems: List<HomeCatalogSettingsItem>,
|
||||||
metaScreenSettingsUiState: MetaScreenSettingsUiState,
|
metaScreenSettingsUiState: MetaScreenSettingsUiState,
|
||||||
continueWatchingPreferencesUiState: ContinueWatchingPreferencesUiState,
|
continueWatchingPreferencesUiState: ContinueWatchingPreferencesUiState,
|
||||||
|
posterCardStyleUiState: PosterCardStyleUiState,
|
||||||
onSwitchProfile: (() -> Unit)? = null,
|
onSwitchProfile: (() -> Unit)? = null,
|
||||||
onHomescreenClick: () -> Unit = {},
|
onHomescreenClick: () -> Unit = {},
|
||||||
onMetaScreenClick: () -> Unit = {},
|
onMetaScreenClick: () -> Unit = {},
|
||||||
|
|
@ -318,6 +327,7 @@ private fun MobileSettingsScreen(
|
||||||
amoledEnabled = amoledEnabled,
|
amoledEnabled = amoledEnabled,
|
||||||
onAmoledToggle = onAmoledToggle,
|
onAmoledToggle = onAmoledToggle,
|
||||||
onContinueWatchingClick = onContinueWatchingClick,
|
onContinueWatchingClick = onContinueWatchingClick,
|
||||||
|
onPosterCustomizationClick = { onPageChange(SettingsPage.PosterCustomization) },
|
||||||
)
|
)
|
||||||
SettingsPage.Notifications -> notificationsSettingsContent(
|
SettingsPage.Notifications -> notificationsSettingsContent(
|
||||||
isTablet = false,
|
isTablet = false,
|
||||||
|
|
@ -330,6 +340,10 @@ private fun MobileSettingsScreen(
|
||||||
upNextFromFurthestEpisode = continueWatchingPreferencesUiState.upNextFromFurthestEpisode,
|
upNextFromFurthestEpisode = continueWatchingPreferencesUiState.upNextFromFurthestEpisode,
|
||||||
showResumePromptOnLaunch = continueWatchingPreferencesUiState.showResumePromptOnLaunch,
|
showResumePromptOnLaunch = continueWatchingPreferencesUiState.showResumePromptOnLaunch,
|
||||||
)
|
)
|
||||||
|
SettingsPage.PosterCustomization -> posterCustomizationSettingsContent(
|
||||||
|
isTablet = false,
|
||||||
|
uiState = posterCardStyleUiState,
|
||||||
|
)
|
||||||
SettingsPage.ContentDiscovery -> contentDiscoveryContent(
|
SettingsPage.ContentDiscovery -> contentDiscoveryContent(
|
||||||
isTablet = false,
|
isTablet = false,
|
||||||
showPluginsEntry = AppFeaturePolicy.pluginsEnabled,
|
showPluginsEntry = AppFeaturePolicy.pluginsEnabled,
|
||||||
|
|
@ -404,6 +418,7 @@ private fun TabletSettingsScreen(
|
||||||
homescreenItems: List<HomeCatalogSettingsItem>,
|
homescreenItems: List<HomeCatalogSettingsItem>,
|
||||||
metaScreenSettingsUiState: MetaScreenSettingsUiState,
|
metaScreenSettingsUiState: MetaScreenSettingsUiState,
|
||||||
continueWatchingPreferencesUiState: ContinueWatchingPreferencesUiState,
|
continueWatchingPreferencesUiState: ContinueWatchingPreferencesUiState,
|
||||||
|
posterCardStyleUiState: PosterCardStyleUiState,
|
||||||
onSwitchProfile: (() -> Unit)? = null,
|
onSwitchProfile: (() -> Unit)? = null,
|
||||||
onDownloadsClick: () -> Unit = {},
|
onDownloadsClick: () -> Unit = {},
|
||||||
onCollectionsClick: () -> Unit = {},
|
onCollectionsClick: () -> Unit = {},
|
||||||
|
|
@ -526,6 +541,7 @@ private fun TabletSettingsScreen(
|
||||||
amoledEnabled = amoledEnabled,
|
amoledEnabled = amoledEnabled,
|
||||||
onAmoledToggle = onAmoledToggle,
|
onAmoledToggle = onAmoledToggle,
|
||||||
onContinueWatchingClick = { openInlinePage(SettingsPage.ContinueWatching) },
|
onContinueWatchingClick = { openInlinePage(SettingsPage.ContinueWatching) },
|
||||||
|
onPosterCustomizationClick = { openInlinePage(SettingsPage.PosterCustomization) },
|
||||||
)
|
)
|
||||||
SettingsPage.Notifications -> notificationsSettingsContent(
|
SettingsPage.Notifications -> notificationsSettingsContent(
|
||||||
isTablet = true,
|
isTablet = true,
|
||||||
|
|
@ -538,6 +554,10 @@ private fun TabletSettingsScreen(
|
||||||
upNextFromFurthestEpisode = continueWatchingPreferencesUiState.upNextFromFurthestEpisode,
|
upNextFromFurthestEpisode = continueWatchingPreferencesUiState.upNextFromFurthestEpisode,
|
||||||
showResumePromptOnLaunch = continueWatchingPreferencesUiState.showResumePromptOnLaunch,
|
showResumePromptOnLaunch = continueWatchingPreferencesUiState.showResumePromptOnLaunch,
|
||||||
)
|
)
|
||||||
|
SettingsPage.PosterCustomization -> posterCustomizationSettingsContent(
|
||||||
|
isTablet = true,
|
||||||
|
uiState = posterCardStyleUiState,
|
||||||
|
)
|
||||||
SettingsPage.ContentDiscovery -> contentDiscoveryContent(
|
SettingsPage.ContentDiscovery -> contentDiscoveryContent(
|
||||||
isTablet = true,
|
isTablet = true,
|
||||||
showPluginsEntry = AppFeaturePolicy.pluginsEnabled,
|
showPluginsEntry = AppFeaturePolicy.pluginsEnabled,
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ internal actual object PlatformLocalAccountDataCleaner {
|
||||||
private val profileScopedBaseKeys = listOf(
|
private val profileScopedBaseKeys = listOf(
|
||||||
"catalog_settings_payload",
|
"catalog_settings_payload",
|
||||||
"continue_watching_preferences_payload",
|
"continue_watching_preferences_payload",
|
||||||
|
"poster_card_style_payload",
|
||||||
"episode_release_notifications_payload",
|
"episode_release_notifications_payload",
|
||||||
"episode_release_notification_scheduled_ids",
|
"episode_release_notification_scheduled_ids",
|
||||||
"selected_theme",
|
"selected_theme",
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
package com.nuvio.app.core.ui
|
||||||
|
|
||||||
|
import com.nuvio.app.core.storage.ProfileScopedKey
|
||||||
|
import platform.Foundation.NSUserDefaults
|
||||||
|
|
||||||
|
actual object PosterCardStyleStorage {
|
||||||
|
private const val payloadKey = "poster_card_style_payload"
|
||||||
|
|
||||||
|
actual fun loadPayload(): String? =
|
||||||
|
NSUserDefaults.standardUserDefaults.stringForKey(ProfileScopedKey.of(payloadKey))
|
||||||
|
|
||||||
|
actual fun savePayload(payload: String) {
|
||||||
|
NSUserDefaults.standardUserDefaults.setObject(payload, forKey = ProfileScopedKey.of(payloadKey))
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue