From a90ee2fcfb54f04d8bc79d10eb254ff815f13639 Mon Sep 17 00:00:00 2001 From: tapframe <85391825+tapframe@users.noreply.github.com> Date: Sat, 11 Jul 2026 12:11:25 +0530 Subject: [PATCH 1/6] feat: add card depth customization --- .../kotlin/com/nuvio/app/MainActivity.kt | 2 + .../core/ui/CardDepthStyleStorage.android.kt | 26 + .../composeResources/values/strings.xml | 31 ++ .../core/storage/LocalAccountDataCleaner.kt | 2 + .../app/core/sync/ProfileSettingsSync.kt | 10 + .../com/nuvio/app/core/ui/CardDepthEffect.kt | 96 ++++ .../app/core/ui/CardDepthStyleRepository.kt | 175 ++++++ .../app/core/ui/CardDepthStyleStorage.kt | 6 + .../com/nuvio/app/core/ui/ShelfComponents.kt | 4 + .../details/components/DetailCastSection.kt | 6 + .../details/components/DetailSeriesContent.kt | 20 +- .../components/DetailTrailersSection.kt | 6 + .../components/HomeContinueWatchingSection.kt | 6 + .../features/profiles/ProfileRepository.kt | 2 + .../PosterCustomizationSettingsPage.kt | 509 ++++++++++++++++++ .../app/features/settings/SettingsSearch.kt | 17 + .../app/core/ui/CardDepthStyleStorage.ios.kt | 15 + 17 files changed, 924 insertions(+), 9 deletions(-) create mode 100644 composeApp/src/androidMain/kotlin/com/nuvio/app/core/ui/CardDepthStyleStorage.android.kt create mode 100644 composeApp/src/commonMain/kotlin/com/nuvio/app/core/ui/CardDepthEffect.kt create mode 100644 composeApp/src/commonMain/kotlin/com/nuvio/app/core/ui/CardDepthStyleRepository.kt create mode 100644 composeApp/src/commonMain/kotlin/com/nuvio/app/core/ui/CardDepthStyleStorage.kt create mode 100644 composeApp/src/iosMain/kotlin/com/nuvio/app/core/ui/CardDepthStyleStorage.ios.kt diff --git a/composeApp/src/androidMain/kotlin/com/nuvio/app/MainActivity.kt b/composeApp/src/androidMain/kotlin/com/nuvio/app/MainActivity.kt index 1c54fd0d3..221e77620 100644 --- a/composeApp/src/androidMain/kotlin/com/nuvio/app/MainActivity.kt +++ b/composeApp/src/androidMain/kotlin/com/nuvio/app/MainActivity.kt @@ -48,6 +48,7 @@ import com.nuvio.app.features.trakt.TraktLibraryStorage import com.nuvio.app.features.trakt.TraktSettingsStorage import com.nuvio.app.features.tmdb.TmdbSettingsStorage import com.nuvio.app.features.updater.AndroidAppUpdaterPlatform +import com.nuvio.app.core.ui.CardDepthStyleStorage import com.nuvio.app.core.ui.PosterCardStyleStorage import com.nuvio.app.features.watched.WatchedStorage import com.nuvio.app.features.streams.StreamLinkCacheStorage @@ -90,6 +91,7 @@ class MainActivity : AppCompatActivity() { SearchHistoryStorage.initialize(applicationContext) SeasonViewModeStorage.initialize(applicationContext) PosterCardStyleStorage.initialize(applicationContext) + CardDepthStyleStorage.initialize(applicationContext) DebridSettingsStorage.initialize(applicationContext) TmdbSettingsStorage.initialize(applicationContext) MdbListSettingsStorage.initialize(applicationContext) diff --git a/composeApp/src/androidMain/kotlin/com/nuvio/app/core/ui/CardDepthStyleStorage.android.kt b/composeApp/src/androidMain/kotlin/com/nuvio/app/core/ui/CardDepthStyleStorage.android.kt new file mode 100644 index 000000000..a0eaa831e --- /dev/null +++ b/composeApp/src/androidMain/kotlin/com/nuvio/app/core/ui/CardDepthStyleStorage.android.kt @@ -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 + +internal actual object CardDepthStyleStorage { + private const val preferencesName = "nuvio_card_depth_style" + private const val payloadKey = "card_depth_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() + } +} diff --git a/composeApp/src/commonMain/composeResources/values/strings.xml b/composeApp/src/commonMain/composeResources/values/strings.xml index 77f6c9e63..e06f8af69 100644 --- a/composeApp/src/commonMain/composeResources/values/strings.xml +++ b/composeApp/src/commonMain/composeResources/values/strings.xml @@ -601,6 +601,37 @@ Visible Hide value Player, subtitles, and auto-play + Card Depth Effect + Adds a lit top edge and a soft sheen to image cards for a subtle sense of depth. + Enable depth effect + Edge glow + Subtle + Balanced + Bold + Top sheen + Off + Soft + Bright + Apply to + Fine-tune + Fine-tune Depth + Drag the dot right for a brighter edge, up for more sheen. Use the slider to extend the glow around the full outline. + Edge glow → + ↑ Sheen + Edge glow + Sheen + Edge coverage + Top only + Half + Full outline + Edge coverage + Episode Title + S1 · E1 · 45 min + Posters + Continue Watching + Episode cards + Cast + Trailers Corner Radius Poster Card Style Width diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/core/storage/LocalAccountDataCleaner.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/core/storage/LocalAccountDataCleaner.kt index c3eff7ba8..68fa9f0af 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/core/storage/LocalAccountDataCleaner.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/core/storage/LocalAccountDataCleaner.kt @@ -27,6 +27,7 @@ import com.nuvio.app.features.streams.StreamLaunchStore import com.nuvio.app.features.streams.StreamsRepository import com.nuvio.app.features.trakt.TraktAuthRepository import com.nuvio.app.features.trakt.TraktSettingsRepository +import com.nuvio.app.core.ui.CardDepthStyleRepository import com.nuvio.app.core.ui.PosterCardStyleRepository import com.nuvio.app.features.watchprogress.ContinueWatchingPreferencesRepository import com.nuvio.app.features.watchprogress.ContinueWatchingEnrichmentCache @@ -61,6 +62,7 @@ internal object LocalAccountDataCleaner { CollectionRepository.clearLocalState() ThemeSettingsRepository.clearLocalState() PosterCardStyleRepository.clearLocalState() + CardDepthStyleRepository.clearLocalState() TraktAuthRepository.clearLocalState() TraktSettingsRepository.clearLocalState() PlayerSettingsRepository.clearLocalState() diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/core/sync/ProfileSettingsSync.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/core/sync/ProfileSettingsSync.kt index d1d566b7a..11fe09158 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/core/sync/ProfileSettingsSync.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/core/sync/ProfileSettingsSync.kt @@ -17,6 +17,8 @@ import com.nuvio.app.features.notifications.EpisodeReleaseNotificationsRepositor import com.nuvio.app.features.player.PlayerSettingsStorage import com.nuvio.app.features.player.PlayerSettingsRepository import com.nuvio.app.features.profiles.ProfileRepository +import com.nuvio.app.core.ui.CardDepthStyleRepository +import com.nuvio.app.core.ui.CardDepthStyleStorage import com.nuvio.app.core.ui.PosterCardStyleRepository import com.nuvio.app.core.ui.PosterCardStyleStorage import com.nuvio.app.features.settings.ThemeSettingsStorage @@ -289,6 +291,7 @@ object ProfileSettingsSync { ThemeSettingsRepository.amoledEnabled.map { "amoled" }, ThemeSettingsRepository.liquidGlassNativeTabBarEnabled.map { "liquid_glass_tab_bar" }, PosterCardStyleRepository.uiState.map { "poster_card_style" }, + CardDepthStyleRepository.uiState.map { "card_depth_style" }, PlayerSettingsRepository.uiState.map { "player" }, StreamBadgeSettingsRepository.uiState.map { "stream_badges" }, DebridSettingsRepository.uiState.map { "debrid" }, @@ -462,6 +465,7 @@ object ProfileSettingsSync { features = MobileProfileSettingsFeatures( themeSettings = ThemeSettingsStorage.exportToSyncPayload(), posterCardStyleSettingsPayload = PosterCardStyleStorage.loadPayload().orEmpty().trim(), + cardDepthStyleSettingsPayload = CardDepthStyleStorage.loadPayload().orEmpty().trim(), playerSettings = PlayerSettingsStorage.exportToSyncPayload(), streamBadgeSettings = StreamBadgeSettingsStorage.exportToSyncPayload(), debridSettings = DebridSettingsStorage.exportToSyncPayload(), @@ -486,6 +490,9 @@ object ProfileSettingsSync { PosterCardStyleStorage.savePayload(blob.features.posterCardStyleSettingsPayload) PosterCardStyleRepository.onProfileChanged() + CardDepthStyleStorage.savePayload(blob.features.cardDepthStyleSettingsPayload) + CardDepthStyleRepository.onProfileChanged() + PlayerSettingsStorage.replaceFromSyncPayload(blob.features.playerSettings) PlayerSettingsRepository.onProfileChanged() @@ -523,6 +530,7 @@ object ProfileSettingsSync { private fun ensureRepositoriesLoaded() { ThemeSettingsRepository.ensureLoaded() PosterCardStyleRepository.ensureLoaded() + CardDepthStyleRepository.ensureLoaded() PlayerSettingsRepository.ensureLoaded() StreamBadgeSettingsRepository.ensureLoaded() DebridSettingsRepository.ensureLoaded() @@ -547,6 +555,7 @@ object ProfileSettingsSync { "amoled=${ThemeSettingsRepository.amoledEnabled.value}", "liquid_glass_tab_bar=${ThemeSettingsRepository.liquidGlassNativeTabBarEnabled.value}", "poster_card_style=${PosterCardStyleRepository.uiState.value}", + "card_depth_style=${CardDepthStyleRepository.uiState.value}", "player=${PlayerSettingsRepository.uiState.value}", "stream_badges=${StreamBadgeSettingsRepository.uiState.value}", "debrid=${DebridSettingsRepository.uiState.value}", @@ -579,6 +588,7 @@ private data class MobileProfileSettingsBlob( private data class MobileProfileSettingsFeatures( @SerialName("theme_settings") val themeSettings: JsonObject = JsonObject(emptyMap()), @SerialName("poster_card_style_settings_payload") val posterCardStyleSettingsPayload: String = "", + @SerialName("card_depth_style_settings_payload") val cardDepthStyleSettingsPayload: String = "", @SerialName("player_settings") val playerSettings: JsonObject = JsonObject(emptyMap()), @SerialName("stream_badge_settings") val streamBadgeSettings: JsonObject = JsonObject(emptyMap()), @SerialName("debrid_settings") val debridSettings: JsonObject = JsonObject(emptyMap()), diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/core/ui/CardDepthEffect.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/core/ui/CardDepthEffect.kt new file mode 100644 index 000000000..ed9581ee7 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/core/ui/CardDepthEffect.kt @@ -0,0 +1,96 @@ +package com.nuvio.app.core.ui + +import androidx.compose.foundation.border +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.drawWithContent +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.unit.dp + +@Composable +fun rememberCardDepthStyleUiState(): CardDepthStyleUiState { + CardDepthStyleRepository.ensureLoaded() + val uiState by CardDepthStyleRepository.uiState.collectAsState() + return uiState +} + +@Composable +fun Modifier.nuvioCardDepth( + shape: Shape, + surface: NuvioCardDepthSurface, + fallbackBorderAlpha: Float = 0f, +): Modifier { + val state = rememberCardDepthStyleUiState() + if (!state.isEnabledFor(surface)) { + return if (fallbackBorderAlpha > 0f) { + border( + width = 1.dp, + color = Color.White.copy(alpha = fallbackBorderAlpha), + shape = shape, + ) + } else { + this + } + } + + return cardDepthVisual( + shape = shape, + edgeStrength = state.edgeStrength.toFloat(), + sheenStrength = state.sheenStrength.toFloat(), + edgeCoverage = state.edgeCoverage.toFloat(), + ) +} + +fun Modifier.cardDepthVisual( + shape: Shape, + edgeStrength: Float, + sheenStrength: Float, + edgeCoverage: Float = DefaultCardDepthEdgeCoverage.toFloat(), +): Modifier { + val edgeTop = edgeStrength.coerceIn(0f, 100f) / 100f + val sheen = sheenStrength.coerceIn(0f, 100f) / 100f + val coverage = edgeCoverage.coerceIn(0f, 100f) / 100f + + val withEdge = if (edgeTop > 0f) { + border( + width = 1.dp, + brush = Brush.verticalGradient( + colors = listOf( + Color.White.copy(alpha = edgeTop), + Color.White.copy(alpha = edgeTop * (0.33f + 0.67f * coverage)), + Color.White.copy(alpha = edgeTop * coverage), + ), + ), + shape = shape, + ) + } else { + this + } + + return if (sheen > 0f) { + withEdge.drawWithContent { + drawContent() + val sheenHeight = size.height * 0.22f + if (sheenHeight > 0f) { + drawRect( + brush = Brush.verticalGradient( + colors = listOf( + Color.White.copy(alpha = sheen), + Color.Transparent, + ), + startY = 0f, + endY = sheenHeight, + ), + size = Size(size.width, sheenHeight), + ) + } + } + } else { + withEdge + } +} diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/core/ui/CardDepthStyleRepository.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/core/ui/CardDepthStyleRepository.kt new file mode 100644 index 000000000..bb73c3059 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/core/ui/CardDepthStyleRepository.kt @@ -0,0 +1,175 @@ +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 DefaultCardDepthEdgeStrength = 28 +internal const val DefaultCardDepthSheenStrength = 10 +internal const val DefaultCardDepthEdgeCoverage = 0 + +enum class NuvioCardDepthSurface { + Posters, + ContinueWatching, + EpisodeCards, + Cast, + Trailers, +} + +@Serializable +private data class StoredCardDepthStylePreferences( + val enabled: Boolean = false, + val edgeStrength: Int = DefaultCardDepthEdgeStrength, + val sheenStrength: Int = DefaultCardDepthSheenStrength, + val edgeCoverage: Int = DefaultCardDepthEdgeCoverage, + val postersEnabled: Boolean = true, + val continueWatchingEnabled: Boolean = true, + val episodeCardsEnabled: Boolean = true, + val castEnabled: Boolean = true, + val trailersEnabled: Boolean = true, +) + +data class CardDepthStyleUiState( + val enabled: Boolean = false, + val edgeStrength: Int = DefaultCardDepthEdgeStrength, + val sheenStrength: Int = DefaultCardDepthSheenStrength, + val edgeCoverage: Int = DefaultCardDepthEdgeCoverage, + val postersEnabled: Boolean = true, + val continueWatchingEnabled: Boolean = true, + val episodeCardsEnabled: Boolean = true, + val castEnabled: Boolean = true, + val trailersEnabled: Boolean = true, +) { + fun isEnabledFor(surface: NuvioCardDepthSurface): Boolean = + enabled && isSurfaceEnabled(surface) + + fun isSurfaceEnabled(surface: NuvioCardDepthSurface): Boolean = + when (surface) { + NuvioCardDepthSurface.Posters -> postersEnabled + NuvioCardDepthSurface.ContinueWatching -> continueWatchingEnabled + NuvioCardDepthSurface.EpisodeCards -> episodeCardsEnabled + NuvioCardDepthSurface.Cast -> castEnabled + NuvioCardDepthSurface.Trailers -> trailersEnabled + } +} + +object CardDepthStyleRepository { + private val json = Json { + ignoreUnknownKeys = true + encodeDefaults = true + } + + private val _uiState = MutableStateFlow(CardDepthStyleUiState()) + val uiState: StateFlow = _uiState.asStateFlow() + + private var hasLoaded = false + + fun ensureLoaded() { + if (hasLoaded) return + loadFromDisk() + } + + fun onProfileChanged() { + loadFromDisk() + } + + fun clearLocalState() { + hasLoaded = false + _uiState.value = CardDepthStyleUiState() + } + + fun setEnabled(enabled: Boolean) { + update { it.copy(enabled = enabled) } + } + + fun setEdgeStrength(strength: Int) { + update { it.copy(edgeStrength = strength.coerceIn(0, 100)) } + } + + fun setSheenStrength(strength: Int) { + update { it.copy(sheenStrength = strength.coerceIn(0, 100)) } + } + + fun setEdgeCoverage(coverage: Int) { + update { it.copy(edgeCoverage = coverage.coerceIn(0, 100)) } + } + + fun setSurfaceEnabled(surface: NuvioCardDepthSurface, enabled: Boolean) { + update { + when (surface) { + NuvioCardDepthSurface.Posters -> it.copy(postersEnabled = enabled) + NuvioCardDepthSurface.ContinueWatching -> it.copy(continueWatchingEnabled = enabled) + NuvioCardDepthSurface.EpisodeCards -> it.copy(episodeCardsEnabled = enabled) + NuvioCardDepthSurface.Cast -> it.copy(castEnabled = enabled) + NuvioCardDepthSurface.Trailers -> it.copy(trailersEnabled = enabled) + } + } + } + + fun resetToDefaults() { + ensureLoaded() + if (_uiState.value == CardDepthStyleUiState()) return + _uiState.value = CardDepthStyleUiState() + persist() + } + + private fun update(transform: (CardDepthStyleUiState) -> CardDepthStyleUiState) { + ensureLoaded() + val next = transform(_uiState.value) + if (_uiState.value == next) return + _uiState.value = next + persist() + } + + private fun loadFromDisk() { + hasLoaded = true + + val payload = CardDepthStyleStorage.loadPayload().orEmpty().trim() + if (payload.isEmpty()) { + _uiState.value = CardDepthStyleUiState() + return + } + + val stored = runCatching { + json.decodeFromString(payload) + }.getOrNull() + + _uiState.value = if (stored != null) { + CardDepthStyleUiState( + enabled = stored.enabled, + edgeStrength = stored.edgeStrength.coerceIn(0, 100), + sheenStrength = stored.sheenStrength.coerceIn(0, 100), + edgeCoverage = stored.edgeCoverage.coerceIn(0, 100), + postersEnabled = stored.postersEnabled, + continueWatchingEnabled = stored.continueWatchingEnabled, + episodeCardsEnabled = stored.episodeCardsEnabled, + castEnabled = stored.castEnabled, + trailersEnabled = stored.trailersEnabled, + ) + } else { + CardDepthStyleUiState() + } + } + + private fun persist() { + CardDepthStyleStorage.savePayload( + json.encodeToString( + StoredCardDepthStylePreferences( + enabled = _uiState.value.enabled, + edgeStrength = _uiState.value.edgeStrength, + sheenStrength = _uiState.value.sheenStrength, + edgeCoverage = _uiState.value.edgeCoverage, + postersEnabled = _uiState.value.postersEnabled, + continueWatchingEnabled = _uiState.value.continueWatchingEnabled, + episodeCardsEnabled = _uiState.value.episodeCardsEnabled, + castEnabled = _uiState.value.castEnabled, + trailersEnabled = _uiState.value.trailersEnabled, + ), + ), + ) + } +} diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/core/ui/CardDepthStyleStorage.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/core/ui/CardDepthStyleStorage.kt new file mode 100644 index 000000000..055940f23 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/core/ui/CardDepthStyleStorage.kt @@ -0,0 +1,6 @@ +package com.nuvio.app.core.ui + +internal expect object CardDepthStyleStorage { + fun loadPayload(): String? + fun savePayload(payload: String) +} diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/core/ui/ShelfComponents.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/core/ui/ShelfComponents.kt index f7b8aa6c4..df5f05de6 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/core/ui/ShelfComponents.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/core/ui/ShelfComponents.kt @@ -148,6 +148,10 @@ fun NuvioPosterCard( .aspectRatio(shape.aspectRatio) .clip(cardShape) .background(tokens.colors.surface) + .nuvioCardDepth( + shape = cardShape, + surface = NuvioCardDepthSurface.Posters, + ) .posterCardClickable( onClick = onClick, onLongClick = onLongClick, diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/components/DetailCastSection.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/components/DetailCastSection.kt index 306152a5e..34de39b12 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/components/DetailCastSection.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/components/DetailCastSection.kt @@ -32,6 +32,8 @@ import androidx.compose.ui.unit.sp import coil3.compose.AsyncImage import coil3.compose.LocalPlatformContext import coil3.request.ImageRequest +import com.nuvio.app.core.ui.NuvioCardDepthSurface +import com.nuvio.app.core.ui.nuvioCardDepth import com.nuvio.app.features.details.MetaPerson import com.nuvio.app.features.details.castAvatarSharedTransitionKey import nuvio.composeapp.generated.resources.* @@ -143,6 +145,10 @@ private fun CastItem( .background( color = MaterialTheme.colorScheme.surfaceVariant, shape = CircleShape, + ) + .nuvioCardDepth( + shape = CircleShape, + surface = NuvioCardDepthSurface.Cast, ), contentAlignment = Alignment.Center, ) { diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/components/DetailSeriesContent.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/components/DetailSeriesContent.kt index a6fdc974b..039e10bd6 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/components/DetailSeriesContent.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/components/DetailSeriesContent.kt @@ -64,7 +64,9 @@ import com.nuvio.app.core.build.AppFeaturePolicy import com.nuvio.app.core.format.formatReleaseDateForDisplay import com.nuvio.app.core.i18n.localizedSeasonEpisodeCode import com.nuvio.app.core.ui.NuvioAnimatedWatchedBadge +import com.nuvio.app.core.ui.NuvioCardDepthSurface import com.nuvio.app.core.ui.NuvioProgressBar +import com.nuvio.app.core.ui.nuvioCardDepth import com.nuvio.app.core.ui.posterCardClickable import com.nuvio.app.features.details.MetaDetails import com.nuvio.app.features.details.MetaEpisodeCardStyle @@ -670,10 +672,10 @@ private fun EpisodeHorizontalCard( .height(metrics.cardHeight) .clip(cardShape) .background(MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.45f)) - .border( - width = 1.dp, - color = Color.White.copy(alpha = 0.12f), + .nuvioCardDepth( shape = cardShape, + surface = NuvioCardDepthSurface.EpisodeCards, + fallbackBorderAlpha = 0.12f, ) .posterCardClickable( onClick = onClick, @@ -699,12 +701,12 @@ private fun EpisodeHorizontalCard( .fillMaxSize() .background( Brush.verticalGradient( - colors = listOf( - Color.Transparent, - Color.Black.copy(alpha = 0.10f), - Color.Black.copy(alpha = 0.42f), - Color.Black.copy(alpha = 0.78f), - ), + 0f to Color.Transparent, + 0.42f to Color.Transparent, + 0.56f to Color.Black.copy(alpha = 0.20f), + 0.70f to Color.Black.copy(alpha = 0.45f), + 0.84f to Color.Black.copy(alpha = 0.68f), + 1f to Color.Black.copy(alpha = 0.92f), ), ), ) diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/components/DetailTrailersSection.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/components/DetailTrailersSection.kt index e9ef5fa82..5ca4fa96f 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/components/DetailTrailersSection.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/components/DetailTrailersSection.kt @@ -37,6 +37,8 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import coil3.compose.AsyncImage +import com.nuvio.app.core.ui.NuvioCardDepthSurface +import com.nuvio.app.core.ui.nuvioCardDepth import com.nuvio.app.features.details.MetaTrailer import nuvio.composeapp.generated.resources.* import nuvio.composeapp.generated.resources.detail_tab_trailer @@ -193,6 +195,10 @@ private fun TrailerCard( modifier = Modifier .fillMaxWidth() .clip(RoundedCornerShape(cornerRadius)) + .nuvioCardDepth( + shape = RoundedCornerShape(cornerRadius), + surface = NuvioCardDepthSurface.Trailers, + ) .clickable(onClick = onClick), ) { AsyncImage( diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/home/components/HomeContinueWatchingSection.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/home/components/HomeContinueWatchingSection.kt index bb0951e3b..a906d0033 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/home/components/HomeContinueWatchingSection.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/home/components/HomeContinueWatchingSection.kt @@ -48,7 +48,9 @@ import androidx.compose.ui.unit.sp import androidx.lifecycle.compose.collectAsStateWithLifecycle import coil3.compose.AsyncImage import com.nuvio.app.core.ui.DisintegratingContainer +import com.nuvio.app.core.ui.NuvioCardDepthSurface import com.nuvio.app.core.ui.NuvioProgressBar +import com.nuvio.app.core.ui.nuvioCardDepth import com.nuvio.app.core.ui.NuvioShelfSection import com.nuvio.app.core.ui.NuvioTokens import com.nuvio.app.core.ui.PosterLandscapeAspectRatio @@ -703,6 +705,10 @@ private fun ContinueWatchingCard( .aspectRatio(PosterLandscapeAspectRatio) .clip(RoundedCornerShape(cardMetrics.cornerRadius)) .background(MaterialTheme.colorScheme.surfaceVariant) + .nuvioCardDepth( + shape = RoundedCornerShape(cardMetrics.cornerRadius), + surface = NuvioCardDepthSurface.ContinueWatching, + ) .posterCardClickable( onClick = onClick, onLongClick = onLongClick, diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/profiles/ProfileRepository.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/profiles/ProfileRepository.kt index cd727a95c..fd76b3fef 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/profiles/ProfileRepository.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/profiles/ProfileRepository.kt @@ -13,6 +13,7 @@ import com.nuvio.app.features.downloads.DownloadsRepository import com.nuvio.app.features.details.MetaScreenSettingsRepository import com.nuvio.app.features.home.HomeCatalogSettingsRepository import com.nuvio.app.features.home.HomeRepository +import com.nuvio.app.core.ui.CardDepthStyleRepository import com.nuvio.app.core.ui.PosterCardStyleRepository import com.nuvio.app.features.library.LibraryRepository import com.nuvio.app.features.mdblist.MdbListSettingsRepository @@ -162,6 +163,7 @@ object ProfileRepository { } ThemeSettingsRepository.onProfileChanged() PosterCardStyleRepository.onProfileChanged() + CardDepthStyleRepository.onProfileChanged() PlayerSettingsRepository.onProfileChanged() StreamBadgeSettingsRepository.onProfileChanged() P2pSettingsRepository.onProfileChanged() diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/PosterCustomizationSettingsPage.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/PosterCustomizationSettingsPage.kt index f4f494d27..ccd3fdcfd 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/PosterCustomizationSettingsPage.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/PosterCustomizationSettingsPage.kt @@ -2,8 +2,12 @@ package com.nuvio.app.features.settings import androidx.compose.animation.core.animateDpAsState import androidx.compose.animation.core.tween +import androidx.compose.foundation.Canvas import androidx.compose.foundation.background import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.gestures.detectDragGestures +import androidx.compose.foundation.gestures.detectTapGestures import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -11,6 +15,8 @@ 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.aspectRatio +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding @@ -18,23 +24,81 @@ 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.material.icons.Icons +import androidx.compose.material.icons.rounded.Tune +import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.FilterChip import androidx.compose.material3.FilterChipDefaults +import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Slider +import androidx.compose.material3.SliderDefaults import androidx.compose.material3.Switch import androidx.compose.material3.SwitchDefaults import androidx.compose.material3.Text +import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableFloatStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberUpdatedState +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp +import kotlin.math.roundToInt +import com.nuvio.app.core.ui.CardDepthStyleRepository +import com.nuvio.app.core.ui.CardDepthStyleUiState +import com.nuvio.app.core.ui.DefaultCardDepthEdgeCoverage +import com.nuvio.app.core.ui.DefaultCardDepthEdgeStrength +import com.nuvio.app.core.ui.DefaultCardDepthSheenStrength import com.nuvio.app.core.ui.NuvioActionLabel +import com.nuvio.app.core.ui.NuvioCardDepthSurface +import com.nuvio.app.core.ui.NuvioModalBottomSheet import com.nuvio.app.core.ui.PosterCardStyleRepository import com.nuvio.app.core.ui.PosterCardStyleUiState +import com.nuvio.app.core.ui.cardDepthVisual import nuvio.composeapp.generated.resources.Res import nuvio.composeapp.generated.resources.action_reset +import nuvio.composeapp.generated.resources.settings_card_depth_apply_to +import nuvio.composeapp.generated.resources.settings_card_depth_description +import nuvio.composeapp.generated.resources.settings_card_depth_edge +import nuvio.composeapp.generated.resources.settings_card_depth_edge_balanced +import nuvio.composeapp.generated.resources.settings_card_depth_edge_bold +import nuvio.composeapp.generated.resources.settings_card_depth_edge_subtle +import nuvio.composeapp.generated.resources.settings_card_depth_edge_value +import nuvio.composeapp.generated.resources.settings_card_depth_edge_coverage +import nuvio.composeapp.generated.resources.settings_card_depth_coverage_full +import nuvio.composeapp.generated.resources.settings_card_depth_coverage_half +import nuvio.composeapp.generated.resources.settings_card_depth_coverage_top +import nuvio.composeapp.generated.resources.settings_card_depth_coverage_value +import nuvio.composeapp.generated.resources.settings_card_depth_enabled +import nuvio.composeapp.generated.resources.settings_card_depth_fine_tune +import nuvio.composeapp.generated.resources.settings_card_depth_fine_tune_hint +import nuvio.composeapp.generated.resources.settings_card_depth_fine_tune_title +import nuvio.composeapp.generated.resources.settings_card_depth_pad_edge_axis +import nuvio.composeapp.generated.resources.settings_card_depth_pad_sheen_axis +import nuvio.composeapp.generated.resources.settings_card_depth_preview_meta +import nuvio.composeapp.generated.resources.settings_card_depth_preview_title +import nuvio.composeapp.generated.resources.settings_card_depth_sheen +import nuvio.composeapp.generated.resources.settings_card_depth_sheen_value +import nuvio.composeapp.generated.resources.settings_card_depth_sheen_bright +import nuvio.composeapp.generated.resources.settings_card_depth_sheen_off +import nuvio.composeapp.generated.resources.settings_card_depth_sheen_soft +import nuvio.composeapp.generated.resources.settings_card_depth_surface_cast +import nuvio.composeapp.generated.resources.settings_card_depth_surface_continue_watching +import nuvio.composeapp.generated.resources.settings_card_depth_surface_episodes +import nuvio.composeapp.generated.resources.settings_card_depth_surface_posters +import nuvio.composeapp.generated.resources.settings_card_depth_surface_trailers +import nuvio.composeapp.generated.resources.settings_card_depth_title import nuvio.composeapp.generated.resources.settings_poster_card_radius import nuvio.composeapp.generated.resources.settings_poster_card_style import nuvio.composeapp.generated.resources.settings_poster_card_width @@ -90,6 +154,451 @@ internal fun LazyListScope.posterCustomizationSettingsContent( } } } + item { + CardDepthStyleRepository.ensureLoaded() + val cardDepthState by CardDepthStyleRepository.uiState.collectAsState() + SettingsSection( + title = stringResource(Res.string.settings_card_depth_title), + isTablet = isTablet, + actions = { + NuvioActionLabel( + text = stringResource(Res.string.action_reset), + onClick = CardDepthStyleRepository::resetToDefaults, + ) + }, + ) { + SettingsGroup(isTablet = isTablet) { + CardDepthStyleControls( + isTablet = isTablet, + uiState = cardDepthState, + ) + } + } + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun CardDepthStyleControls( + isTablet: Boolean, + uiState: CardDepthStyleUiState, +) { + var showFineTune by remember { mutableStateOf(false) } + val edgeOptions = listOf( + PresetOption(stringResource(Res.string.settings_card_depth_edge_subtle), 28), + PresetOption(stringResource(Res.string.settings_card_depth_edge_balanced), 42), + PresetOption(stringResource(Res.string.settings_card_depth_edge_bold), 56), + ) + val sheenOptions = listOf( + PresetOption(stringResource(Res.string.settings_card_depth_sheen_off), 0), + PresetOption(stringResource(Res.string.settings_card_depth_sheen_soft), 10), + PresetOption(stringResource(Res.string.settings_card_depth_sheen_bright), 16), + ) + val coverageOptions = listOf( + PresetOption(stringResource(Res.string.settings_card_depth_coverage_top), 0), + PresetOption(stringResource(Res.string.settings_card_depth_coverage_half), 50), + PresetOption(stringResource(Res.string.settings_card_depth_coverage_full), 100), + ) + val surfaceRows = listOf( + stringResource(Res.string.settings_card_depth_surface_posters) to NuvioCardDepthSurface.Posters, + stringResource(Res.string.settings_card_depth_surface_continue_watching) to NuvioCardDepthSurface.ContinueWatching, + stringResource(Res.string.settings_card_depth_surface_episodes) to NuvioCardDepthSurface.EpisodeCards, + stringResource(Res.string.settings_card_depth_surface_cast) to NuvioCardDepthSurface.Cast, + stringResource(Res.string.settings_card_depth_surface_trailers) to NuvioCardDepthSurface.Trailers, + ) + + 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 = stringResource(Res.string.settings_card_depth_description), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + PosterToggleRow( + title = stringResource(Res.string.settings_card_depth_enabled), + checked = uiState.enabled, + onCheckedChange = CardDepthStyleRepository::setEnabled, + ) + if (uiState.enabled) { + PosterStyleOptionRow( + title = stringResource(Res.string.settings_card_depth_edge), + selectedValue = uiState.edgeStrength, + options = edgeOptions, + onSelected = CardDepthStyleRepository::setEdgeStrength, + ) + PosterStyleOptionRow( + title = stringResource(Res.string.settings_card_depth_sheen), + selectedValue = uiState.sheenStrength, + options = sheenOptions, + onSelected = CardDepthStyleRepository::setSheenStrength, + ) + PosterStyleOptionRow( + title = stringResource(Res.string.settings_card_depth_edge_coverage), + selectedValue = uiState.edgeCoverage, + options = coverageOptions, + onSelected = CardDepthStyleRepository::setEdgeCoverage, + ) + Row( + modifier = Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(10.dp)) + .clickable { showFineTune = true }, + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = stringResource(Res.string.settings_card_depth_fine_tune), + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onSurface, + fontWeight = FontWeight.Medium, + ) + Icon( + imageVector = Icons.Rounded.Tune, + contentDescription = null, + tint = MaterialTheme.colorScheme.primary, + ) + } + Text( + text = stringResource(Res.string.settings_card_depth_apply_to), + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onSurface, + fontWeight = FontWeight.SemiBold, + ) + surfaceRows.forEach { (title, surface) -> + PosterToggleRow( + title = title, + checked = uiState.isSurfaceEnabled(surface), + onCheckedChange = { enabled -> + CardDepthStyleRepository.setSurfaceEnabled(surface, enabled) + }, + ) + } + } + } + + if (showFineTune) { + CardDepthFineTuneSheet( + initialEdgeStrength = uiState.edgeStrength, + initialSheenStrength = uiState.sheenStrength, + initialEdgeCoverage = uiState.edgeCoverage, + onDismiss = { showFineTune = false }, + ) + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun CardDepthFineTuneSheet( + initialEdgeStrength: Int, + initialSheenStrength: Int, + initialEdgeCoverage: Int, + onDismiss: () -> Unit, +) { + val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) + var draftEdge by remember { mutableFloatStateOf(initialEdgeStrength.toFloat()) } + var draftSheen by remember { mutableFloatStateOf(initialSheenStrength.toFloat()) } + var draftCoverage by remember { mutableFloatStateOf(initialEdgeCoverage.toFloat()) } + + fun commitDraft() { + CardDepthStyleRepository.setEdgeStrength(draftEdge.roundToInt()) + CardDepthStyleRepository.setSheenStrength(draftSheen.roundToInt()) + CardDepthStyleRepository.setEdgeCoverage(draftCoverage.roundToInt()) + } + + NuvioModalBottomSheet( + onDismissRequest = { + commitDraft() + onDismiss() + }, + sheetState = sheetState, + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(start = 20.dp, end = 20.dp, bottom = 32.dp), + verticalArrangement = Arrangement.spacedBy(16.dp), + ) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = stringResource(Res.string.settings_card_depth_fine_tune_title), + style = MaterialTheme.typography.titleLarge, + color = MaterialTheme.colorScheme.onSurface, + fontWeight = FontWeight.SemiBold, + ) + NuvioActionLabel( + text = stringResource(Res.string.action_reset), + onClick = { + draftEdge = DefaultCardDepthEdgeStrength.toFloat() + draftSheen = DefaultCardDepthSheenStrength.toFloat() + draftCoverage = DefaultCardDepthEdgeCoverage.toFloat() + commitDraft() + }, + ) + } + Text( + text = stringResource(Res.string.settings_card_depth_fine_tune_hint), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + CardDepthPreviewCard( + edgeStrength = draftEdge, + sheenStrength = draftSheen, + edgeCoverage = draftCoverage, + ) + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + ) { + Text( + text = "${stringResource(Res.string.settings_card_depth_edge_value)}: ${formatDepthPercentage(draftEdge)}", + style = MaterialTheme.typography.labelLarge, + color = MaterialTheme.colorScheme.onSurface, + ) + Text( + text = "${stringResource(Res.string.settings_card_depth_sheen_value)}: ${formatDepthPercentage(draftSheen)}", + style = MaterialTheme.typography.labelLarge, + color = MaterialTheme.colorScheme.onSurface, + ) + } + CardDepthTuningPad( + edgeStrength = draftEdge, + sheenStrength = draftSheen, + onChange = { edge, sheen -> + draftEdge = edge + draftSheen = sheen + }, + onCommit = { edge, sheen -> + CardDepthStyleRepository.setEdgeStrength(edge.roundToInt()) + CardDepthStyleRepository.setSheenStrength(sheen.roundToInt()) + }, + ) + Text( + text = "${stringResource(Res.string.settings_card_depth_coverage_value)}: ${formatDepthPercentage(draftCoverage)}", + style = MaterialTheme.typography.labelLarge, + color = MaterialTheme.colorScheme.onSurface, + ) + Slider( + value = draftCoverage, + onValueChange = { draftCoverage = it }, + onValueChangeFinished = { + CardDepthStyleRepository.setEdgeCoverage(draftCoverage.roundToInt()) + }, + valueRange = 0f..100f, + colors = SliderDefaults.colors( + thumbColor = MaterialTheme.colorScheme.primary, + activeTrackColor = MaterialTheme.colorScheme.primary, + ), + modifier = Modifier.fillMaxWidth(), + ) + } + } +} + +private fun formatDepthPercentage(value: Float): String { + val tenths = (value.coerceIn(0f, 100f) * 10f).roundToInt() + return "${tenths / 10}.${tenths % 10}%" +} + +@Composable +private fun CardDepthPreviewCard( + edgeStrength: Float, + sheenStrength: Float, + edgeCoverage: Float, +) { + val shape = RoundedCornerShape(14.dp) + Box( + modifier = Modifier + .fillMaxWidth() + .aspectRatio(16f / 9f) + .clip(shape) + .background( + Brush.linearGradient( + colors = listOf( + Color(0xFF33415C), + Color(0xFF232D42), + Color(0xFF141A28), + ), + ), + ) + .cardDepthVisual( + shape = shape, + edgeStrength = edgeStrength, + sheenStrength = sheenStrength, + edgeCoverage = edgeCoverage, + ), + ) { + Box( + modifier = Modifier + .fillMaxSize() + .background( + Brush.verticalGradient( + 0f to Color.Transparent, + 0.42f to Color.Transparent, + 0.56f to Color.Black.copy(alpha = 0.20f), + 0.70f to Color.Black.copy(alpha = 0.45f), + 0.84f to Color.Black.copy(alpha = 0.68f), + 1f to Color.Black.copy(alpha = 0.92f), + ), + ), + ) + Column( + modifier = Modifier + .align(Alignment.BottomStart) + .padding(14.dp), + verticalArrangement = Arrangement.spacedBy(4.dp), + ) { + Text( + text = stringResource(Res.string.settings_card_depth_preview_meta), + style = MaterialTheme.typography.labelSmall, + color = Color.White.copy(alpha = 0.78f), + ) + Text( + text = stringResource(Res.string.settings_card_depth_preview_title), + style = MaterialTheme.typography.titleMedium, + color = Color.White, + fontWeight = FontWeight.ExtraBold, + ) + } + } +} + +@Composable +private fun CardDepthTuningPad( + edgeStrength: Float, + sheenStrength: Float, + onChange: (Float, Float) -> Unit, + onCommit: (Float, Float) -> Unit, +) { + val maxEdge = 70f + val maxSheen = 25f + val shape = RoundedCornerShape(16.dp) + val thumbColor = MaterialTheme.colorScheme.primary + val gridColor = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.4f) + val currentEdge by rememberUpdatedState(edgeStrength) + val currentSheen by rememberUpdatedState(sheenStrength) + val currentOnChange by rememberUpdatedState(onChange) + val currentOnCommit by rememberUpdatedState(onCommit) + + Box( + modifier = Modifier + .fillMaxWidth() + .height(200.dp) + .clip(shape) + .background(MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.35f)) + .border( + width = 1.dp, + color = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.5f), + shape = shape, + ) + .pointerInput(Unit) { + fun valuesAt(position: Offset): Pair { + val x = (position.x / size.width).coerceIn(0f, 1f) + val y = 1f - (position.y / size.height).coerceIn(0f, 1f) + return (x * maxEdge) to (y * maxSheen) + } + detectDragGestures( + onDragStart = { position -> + val (edge, sheen) = valuesAt(position) + currentOnChange(edge, sheen) + }, + onDrag = { change, _ -> + change.consume() + val (edge, sheen) = valuesAt(change.position) + currentOnChange(edge, sheen) + }, + onDragEnd = { + currentOnCommit(currentEdge, currentSheen) + }, + onDragCancel = { + currentOnCommit(currentEdge, currentSheen) + }, + ) + } + .pointerInput(Unit) { + fun valuesAt(position: Offset): Pair { + val x = (position.x / size.width).coerceIn(0f, 1f) + val y = 1f - (position.y / size.height).coerceIn(0f, 1f) + return (x * maxEdge) to (y * maxSheen) + } + detectTapGestures { position -> + val (edge, sheen) = valuesAt(position) + currentOnChange(edge, sheen) + currentOnCommit(edge, sheen) + } + }, + ) { + Canvas(modifier = Modifier.fillMaxSize()) { + for (step in 1..3) { + val x = size.width * step / 4f + val y = size.height * step / 4f + drawLine( + color = gridColor, + start = Offset(x, 0f), + end = Offset(x, size.height), + strokeWidth = 1.dp.toPx(), + ) + drawLine( + color = gridColor, + start = Offset(0f, y), + end = Offset(size.width, y), + strokeWidth = 1.dp.toPx(), + ) + } + val thumbX = size.width * (edgeStrength / maxEdge).coerceIn(0f, 1f) + val thumbY = size.height * (1f - (sheenStrength / maxSheen).coerceIn(0f, 1f)) + drawLine( + color = thumbColor.copy(alpha = 0.35f), + start = Offset(thumbX, 0f), + end = Offset(thumbX, size.height), + strokeWidth = 1.dp.toPx(), + ) + drawLine( + color = thumbColor.copy(alpha = 0.35f), + start = Offset(0f, thumbY), + end = Offset(size.width, thumbY), + strokeWidth = 1.dp.toPx(), + ) + drawCircle( + color = thumbColor.copy(alpha = 0.25f), + radius = 16.dp.toPx(), + center = Offset(thumbX, thumbY), + ) + drawCircle( + color = thumbColor, + radius = 9.dp.toPx(), + center = Offset(thumbX, thumbY), + ) + drawCircle( + color = Color.White, + radius = 4.dp.toPx(), + center = Offset(thumbX, thumbY), + ) + } + Text( + text = stringResource(Res.string.settings_card_depth_pad_sheen_axis), + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier + .align(Alignment.TopStart) + .padding(10.dp), + ) + Text( + text = stringResource(Res.string.settings_card_depth_pad_edge_axis), + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier + .align(Alignment.BottomEnd) + .padding(10.dp), + ) + } } @OptIn(ExperimentalLayoutApi::class) diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/SettingsSearch.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/SettingsSearch.kt index 95ade90ce..419d5567f 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/SettingsSearch.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/SettingsSearch.kt @@ -710,6 +710,23 @@ internal fun settingsSearchEntries( ) } + val cardDepthSection = stringResource(Res.string.settings_card_depth_title) + listOf( + PlaybackSearchRow("card-depth-effect", cardDepthSection, stringResource(Res.string.settings_card_depth_description)), + PlaybackSearchRow("card-depth-edge", stringResource(Res.string.settings_card_depth_edge)), + PlaybackSearchRow("card-depth-sheen", stringResource(Res.string.settings_card_depth_sheen)), + ).forEach { row -> + addRow( + page = SettingsPage.PosterCustomization, + key = "poster-${row.key}", + title = row.title, + description = row.description, + pageLabel = posterStylePage, + section = cardDepthSection, + icon = Icons.Rounded.Tune, + ) + } + val homeLayoutSection = stringResource(Res.string.settings_homescreen_section_hero) listOf( PlaybackSearchRow("home-hero", stringResource(Res.string.settings_homescreen_show_hero), stringResource(Res.string.settings_homescreen_show_hero_description)), diff --git a/composeApp/src/iosMain/kotlin/com/nuvio/app/core/ui/CardDepthStyleStorage.ios.kt b/composeApp/src/iosMain/kotlin/com/nuvio/app/core/ui/CardDepthStyleStorage.ios.kt new file mode 100644 index 000000000..3a52ff9c0 --- /dev/null +++ b/composeApp/src/iosMain/kotlin/com/nuvio/app/core/ui/CardDepthStyleStorage.ios.kt @@ -0,0 +1,15 @@ +package com.nuvio.app.core.ui + +import com.nuvio.app.core.storage.ProfileScopedKey +import platform.Foundation.NSUserDefaults + +internal actual object CardDepthStyleStorage { + private const val payloadKey = "card_depth_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)) + } +} From d9954c04e887c623b12666fac18359116638aa17 Mon Sep 17 00:00:00 2001 From: tapframe <85391825+tapframe@users.noreply.github.com> Date: Sun, 12 Jul 2026 02:14:35 +0530 Subject: [PATCH 2/6] Refine store copy and empty states --- .../core/build/AppFeaturePolicy.android.kt | 2 +- .../core/build/AppFeaturePolicy.android.kt | 2 +- .../composeResources/values-bg/strings.xml | 16 ++-- .../composeResources/values-cs/strings.xml | 16 ++-- .../composeResources/values-de/strings.xml | 10 +++ .../composeResources/values-el/strings.xml | 16 ++-- .../composeResources/values-es/strings.xml | 16 ++-- .../composeResources/values-fr/strings.xml | 16 ++-- .../composeResources/values-hu/strings.xml | 16 ++-- .../composeResources/values-id/strings.xml | 10 +++ .../composeResources/values-in/strings.xml | 10 +++ .../composeResources/values-it/strings.xml | 10 +++ .../composeResources/values-ja/strings.xml | 16 ++-- .../composeResources/values-nb/strings.xml | 10 +++ .../composeResources/values-nl/strings.xml | 16 ++-- .../composeResources/values-pl/strings.xml | 10 +++ .../values-pt-rBR/strings.xml | 10 +++ .../composeResources/values-pt/strings.xml | 10 +++ .../composeResources/values-ro/strings.xml | 18 ++-- .../composeResources/values-sk/strings.xml | 16 ++-- .../composeResources/values-tr/strings.xml | 10 +++ .../composeResources/values-vi/strings.xml | 18 ++-- .../composeResources/values/strings.xml | 16 ++-- .../nuvio/app/core/build/AppFeaturePolicy.kt | 2 +- .../com/nuvio/app/core/ui/EmptyState.kt | 83 +++++++++++++++++++ .../nuvio/app/features/addons/AddonsScreen.kt | 73 +++++++--------- .../app/features/catalog/CatalogScreen.kt | 29 +++---- .../app/features/downloads/DownloadsScreen.kt | 55 +++--------- .../com/nuvio/app/features/home/HomeScreen.kt | 78 ++++++++++++----- .../home/components/HomeStateCards.kt | 38 +++------ .../app/features/library/LibraryScreen.kt | 27 ++++-- .../features/search/SearchDiscoverContent.kt | 37 +++++++-- .../nuvio/app/features/search/SearchScreen.kt | 41 +++++++-- .../settings/ContentDiscoverySettingsPage.kt | 10 ++- .../settings/HomescreenSettingsPage.kt | 23 ++++- .../app/features/settings/SettingsRootPage.kt | 19 ++++- .../app/features/settings/SettingsScreen.kt | 4 +- .../app/features/settings/SettingsSearch.kt | 41 ++++----- .../app/features/streams/StreamsScreen.kt | 73 ++++++++-------- .../core/build/AppFeaturePolicy.desktop.kt | 2 +- .../app/core/build/AppFeaturePolicy.ios.kt | 2 +- .../app/core/build/AppFeaturePolicy.ios.kt | 2 +- iosApp/iosApp.xcodeproj/project.pbxproj | 2 +- 43 files changed, 616 insertions(+), 315 deletions(-) create mode 100644 composeApp/src/commonMain/kotlin/com/nuvio/app/core/ui/EmptyState.kt diff --git a/composeApp/src/androidFull/kotlin/com/nuvio/app/core/build/AppFeaturePolicy.android.kt b/composeApp/src/androidFull/kotlin/com/nuvio/app/core/build/AppFeaturePolicy.android.kt index 364def91d..f4de81fc1 100644 --- a/composeApp/src/androidFull/kotlin/com/nuvio/app/core/build/AppFeaturePolicy.android.kt +++ b/composeApp/src/androidFull/kotlin/com/nuvio/app/core/build/AppFeaturePolicy.android.kt @@ -4,7 +4,7 @@ actual object AppFeaturePolicy { actual val pluginsEnabled: Boolean = true actual val supportersContributorsPageEnabled: Boolean = true actual val accountDeletionEnabled: Boolean = false - actual val personalMediaAddonCopyEnabled: Boolean = false + actual val storeNarrativeEnabled: Boolean = false actual val p2pEnabled: Boolean = true actual val trailerPlaybackMode: TrailerPlaybackMode = TrailerPlaybackMode.IN_APP actual val heroTrailerPlaybackSupported: Boolean = true diff --git a/composeApp/src/androidPlaystore/kotlin/com/nuvio/app/core/build/AppFeaturePolicy.android.kt b/composeApp/src/androidPlaystore/kotlin/com/nuvio/app/core/build/AppFeaturePolicy.android.kt index f0ee56ff0..3db44aa5e 100644 --- a/composeApp/src/androidPlaystore/kotlin/com/nuvio/app/core/build/AppFeaturePolicy.android.kt +++ b/composeApp/src/androidPlaystore/kotlin/com/nuvio/app/core/build/AppFeaturePolicy.android.kt @@ -4,7 +4,7 @@ actual object AppFeaturePolicy { actual val pluginsEnabled: Boolean = false actual val supportersContributorsPageEnabled: Boolean = true actual val accountDeletionEnabled: Boolean = false - actual val personalMediaAddonCopyEnabled: Boolean = false + actual val storeNarrativeEnabled: Boolean = true actual val p2pEnabled: Boolean = true actual val trailerPlaybackMode: TrailerPlaybackMode = TrailerPlaybackMode.EXTERNAL actual val heroTrailerPlaybackSupported: Boolean = false diff --git a/composeApp/src/commonMain/composeResources/values-bg/strings.xml b/composeApp/src/commonMain/composeResources/values-bg/strings.xml index d02723c2f..b14cd5fff 100644 --- a/composeApp/src/commonMain/composeResources/values-bg/strings.xml +++ b/composeApp/src/commonMain/composeResources/values-bg/strings.xml @@ -433,7 +433,9 @@ Изтегли последна версия Провери за актуализации Управлявай добавки и източници за discovery. + Управлявайте добавките и изберете какво да се показва в Nuvio. Управлявай изтеглените филми и епизоди. + Управлявайте запазеното на това устройство. Изтегляния ОБЩИ Управлявай наличните интеграции @@ -1845,11 +1847,13 @@ Грешка в торента: %1$s Превключи - Свържете собствен медиен сървър, за да разглеждате и възпроизвеждате от личната си библиотека. - Добавете URL на сървър по-горе, когато искате Nuvio да показва вашата лична библиотека. - Няма свързани лични библиотеки. - URL на сървър - Добави + Пренесете библиотеката си в Nuvio с добавките, които използвате. + Добавете добавка, за да започнете да изграждате библиотеката си. + Настройте Nuvio по свой начин. + URL на добавка + Добавяне на добавка + Все още няма нищо тук + Текущите ви добавки все още нямат какво да покажат тук. Сървър и поддръжка за този месец Покрито. Допълнителната подкрепа вече отива за разработка. След 100% допълнителната подкрепа отива за разработка. @@ -1861,7 +1865,7 @@ Още действия Неизвестна грешка в торента Език на устройството - Свържете лични медийни източници и управлявайте достъпа до собствената си библиотека. + Добавяйте и управлявайте добавките, които използвате с Nuvio. Изпращане на времеви маркери за интро и аутро Предайте откритите времеви маркери за интро и аутро към външния плейър за автоматично прескачане. Работи само в плейъри, които го поддържат; останалите плейъри го игнорират. Жестове с докосване diff --git a/composeApp/src/commonMain/composeResources/values-cs/strings.xml b/composeApp/src/commonMain/composeResources/values-cs/strings.xml index 405334190..039e9d5d8 100644 --- a/composeApp/src/commonMain/composeResources/values-cs/strings.xml +++ b/composeApp/src/commonMain/composeResources/values-cs/strings.xml @@ -32,11 +32,13 @@ Nedostupné Konfigurovat doplněk Smazat doplněk - Připojte svůj vlastní mediální server a procházejte i přehrávejte ze své osobní knihovny. - Přidejte výše adresu URL serveru, pokud chcete, aby Nuvio zobrazovalo vaši soukromou knihovnu. - Nejsou připojeny žádné osobní knihovny. - URL serveru - Přidat + Přeneste svou knihovnu do Nuvia pomocí doplňků, které používáte. + Přidejte doplněk a začněte vytvářet svou knihovnu. + Přizpůsobte si Nuvio. + URL doplňku + Přidat doplněk + Zatím tu nic není + Vaše současné doplňky tu zatím nemají co zobrazit. Přidejte URL manifestu, abyste mohli do Nuvio začít načítat katalogy, metadata, streamy nebo titulky. Zatím nejsou nainstalovány žádné doplňky. Zadejte URL doplňku. @@ -442,7 +444,9 @@ Stáhnout nejnovější verzi Zkontrolovat aktualizace Správa doplňků a zdrojů objevování. + Spravujte doplňky a vyberte, co se má v Nuviu zobrazovat. Spravujte své stažené filmy a epizody. + Spravujte obsah uložený v tomto zařízení. Stažené položky OBECNÉ Spravovat dostupné integrace @@ -638,7 +642,7 @@ DOMŮ ZDROJE Instalovat, odstraňovat, obnovovat a třídit vaše zdroje obsahu. - Připojit osobní mediální zdroje a spravovat přístup k vlastní knihovně. + Přidávejte a spravujte doplňky, které používáte s Nuviem. Instalovat repozitáře pro JavaScriptové scarpování a testovat poskytovatele interně. Upravit rozvržení domovské obrazovky, viditelnost obsahu a chování plakátů. Nastavení pro obrazovku detailu a epizody. diff --git a/composeApp/src/commonMain/composeResources/values-de/strings.xml b/composeApp/src/commonMain/composeResources/values-de/strings.xml index 3afb1161c..09a975020 100644 --- a/composeApp/src/commonMain/composeResources/values-de/strings.xml +++ b/composeApp/src/commonMain/composeResources/values-de/strings.xml @@ -27,6 +27,13 @@ Nicht verfügbar Addon konfigurieren Addon löschen + Bring deine Mediathek mit den Addons, die du nutzt, zu Nuvio. + Füge ein Addon hinzu, um deine Mediathek aufzubauen. + Richte Nuvio für dich ein. + Addon-URL + Addon hinzufügen + Hier gibt es noch nichts + Deine aktuellen Addons zeigen hier noch nichts an. Füge eine Manifest-URL hinzu, um Kataloge, Metadaten, Streams oder Untertitel in Nuvio zu laden. Noch keine Addons installiert. Gib eine Addon-URL ein. @@ -383,7 +390,9 @@ Suche nach neuen Versionen der App. Nach Updates suchen Verwalte Addons und Entdeckungsquellen. + Verwalte Addons und wähle aus, was in Nuvio angezeigt wird. Verwalte deine heruntergeladenen Filme und Episoden. + Verwalte, was du auf diesem Gerät gespeichert hast. Downloads ALLGEMEIN TMDB- und MDBList-Dienste verbinden. @@ -519,6 +528,7 @@ START QUELLEN Installiere, entferne, aktualisiere und sortiere deine Inhaltsquellen. + Füge die Addons hinzu, die du mit Nuvio nutzt, und verwalte sie. Installiere JavaScript-Scraper-Repositories und teste Anbieter intern. Lege fest, welche Kataloge auf der Startseite und in welcher Reihenfolge erscheinen. Detail-Abschnitte deaktivieren und alles unterhalb des Hero neu anordnen. diff --git a/composeApp/src/commonMain/composeResources/values-el/strings.xml b/composeApp/src/commonMain/composeResources/values-el/strings.xml index 4c2fd1766..01c48e62d 100644 --- a/composeApp/src/commonMain/composeResources/values-el/strings.xml +++ b/composeApp/src/commonMain/composeResources/values-el/strings.xml @@ -233,7 +233,9 @@ Ελέγξτε για νέες εκδόσεις της εφαρμογής. Έλεγχος για ενημερώσεις Διαχείριση πρόσθετων και πηγών ανακάλυψης. + Διαχειριστείτε τα πρόσθετα και επιλέξτε τι εμφανίζεται στο Nuvio. Διαχειριστείτε τις κατεβασμένες ταινίες και επεισόδιά σας. + Διαχειριστείτε όσα έχετε αποθηκεύσει σε αυτή τη συσκευή. Λήψεις ΓΕΝΙΚΑ Σύνδεση υπηρεσιών TMDB και MDBList. @@ -1061,11 +1063,13 @@ Αποθήκευση… Εναλλαγή Επικύρωση - Συνδέστε τον δικό σας διακομιστή μέσων για να περιηγηθείτε και να αναπαραγάγετε από τη δική σας προσωπική βιβλιοθήκη. - Προσθέστε μια διεύθυνση URL διακομιστή παραπάνω όταν θέλετε το Nuvio να εμφανίζει τη δική σας ιδιωτική βιβλιοθήκη. - Δεν υπάρχουν προσωπικές βιβλιοθήκες συνδεδεμένες. - Διεύθυνση URL διακομιστή - Προσθήκη + Φέρτε τη βιβλιοθήκη σας στο Nuvio με τα πρόσθετα που χρησιμοποιείτε. + Προσθέστε ένα πρόσθετο για να αρχίσετε να δημιουργείτε τη βιβλιοθήκη σας. + Κάντε το Nuvio δικό σας. + URL πρόσθετου + Προσθήκη πρόσθετου + Δεν υπάρχει τίποτα εδώ ακόμη + Τα πρόσθετα που χρησιμοποιείτε δεν έχουν ακόμη τίποτα να εμφανίσουν εδώ. Απενεργοποιημένο Το manifest είναι κενό "%1$s" Σύνδεση λογαριασμού @@ -1520,7 +1524,7 @@ Device Γλώσσα Liquid Glass Use the native iPhone tab bar on iOS 26 and later. - Connect personal media sources and manage access to your own Βιβλιοθήκη. + Προσθέστε και διαχειριστείτε τα πρόσθετα που χρησιμοποιείτε με το Nuvio. Blur next episode thumbnails in Συνέχεια Watching to avoid spoilers. Blur Unwatched in Συνέχεια Watching Ταξινόμηση ORDER diff --git a/composeApp/src/commonMain/composeResources/values-es/strings.xml b/composeApp/src/commonMain/composeResources/values-es/strings.xml index e591930a4..02427ae4a 100644 --- a/composeApp/src/commonMain/composeResources/values-es/strings.xml +++ b/composeApp/src/commonMain/composeResources/values-es/strings.xml @@ -32,11 +32,13 @@ No disponible Configurar complemento Eliminar complemento - Conecte su propio servidor multimedia para navegar y reproducir desde su biblioteca personal. - Agregue una URL de servidor arriba cuando desee que Nuvio muestre su biblioteca privada. - No hay bibliotecas personales conectadas. - URL del servidor - Agregar + Lleva tu biblioteca a Nuvio con los complementos que utilizas. + Agrega un complemento para empezar a crear tu biblioteca. + Haz tuyo Nuvio. + URL del complemento + Agregar complemento + Aún no hay nada aquí + Tus complementos actuales aún no tienen nada que mostrar aquí. Agrega una URL de manifiesto para empezar a cargar catálogos, metadatos, streams o subtítulos en Nuvio. Aún no hay complementos instalados. Introduce una URL de complemento. @@ -442,7 +444,9 @@ Buscar nuevas versiones de la app. Buscar actualizaciones Administra complementos y fuentes de descubrimiento. + Administra tus complementos y elige qué aparece en Nuvio. Administra tus películas y episodios descargados. + Administra lo que has guardado en este dispositivo. Descargas GENERAL Conecta los servicios TMDB y MDBList. @@ -638,7 +642,7 @@ INICIO FUENTES Instala, elimina, actualiza y ordena tus fuentes de contenido. - Conecte fuentes de medios personales y administre el acceso a su propia biblioteca. + Agrega y administra los complementos que utilizas con Nuvio. Instala repositorios de scrapers en JavaScript y prueba proveedores internamente. Controla qué catálogos aparecen en Inicio y en qué orden. Desactiva secciones de detalles y reordena todo debajo del Destacado. diff --git a/composeApp/src/commonMain/composeResources/values-fr/strings.xml b/composeApp/src/commonMain/composeResources/values-fr/strings.xml index ffb979e29..a6a536650 100644 --- a/composeApp/src/commonMain/composeResources/values-fr/strings.xml +++ b/composeApp/src/commonMain/composeResources/values-fr/strings.xml @@ -33,11 +33,13 @@ Indisponible Configurer l’addon Supprimer l’addon - Connectez votre propre serveur multimédia pour parcourir et lire depuis votre bibliothèque personnelle. - Ajoutez une URL de serveur ci-dessus pour que Nuvio affiche votre bibliothèque privée. - Aucune bibliothèque personnelle connectée. - URL du serveur - Ajouter + Retrouvez votre bibliothèque dans Nuvio avec les addons que vous utilisez. + Ajoutez un addon pour commencer à créer votre bibliothèque. + Personnalisez Nuvio. + URL de l’addon + Ajouter un addon + Rien à afficher pour le moment + Vos addons actuels n’ont encore rien à afficher ici. Ajoutez une URL de manifeste pour commencer à charger des catalogues, métadonnées, streams ou sous-titres dans Nuvio. Aucun addon installé. Saisissez une URL d’addon. @@ -434,7 +436,9 @@ Rechercher de nouvelles versions de l’application. Vérifier les mises à jour Gérez les addons et sources de découverte. + Gérez vos addons et choisissez ce qui apparaît dans Nuvio. Gérez vos films et épisodes téléchargés. + Gérez ce que vous avez enregistré sur cet appareil. Téléchargements GÉNÉRAL Connectez les services TMDB et MDBList. @@ -648,7 +652,7 @@ ACCUEIL SOURCES Installez, supprimez, mettez à jour et ordonnez vos sources de contenu. - Connectez des sources multimédias personnelles et gérez l’accès à votre propre bibliothèque. + Ajoutez et gérez les addons que vous utilisez avec Nuvio. Installez des dépôts de scrapers JavaScript et testez des fournisseurs en interne. Contrôle quels catalogues apparaissent à l’accueil et dans quel ordre. Désactivez des sections de détails et réorganisez tout sous le Hero. diff --git a/composeApp/src/commonMain/composeResources/values-hu/strings.xml b/composeApp/src/commonMain/composeResources/values-hu/strings.xml index 6979d2fa7..3dda499ab 100644 --- a/composeApp/src/commonMain/composeResources/values-hu/strings.xml +++ b/composeApp/src/commonMain/composeResources/values-hu/strings.xml @@ -401,7 +401,9 @@ A legújabb verzió letöltése Frissítések keresése Kiegészítők és tartalomforrások kezelése + Kezeld a kiegészítőket, és válaszd ki, mi jelenjen meg a Nuvióban. A letöltött filmek és epizódok kezelése + Kezeld az eszközre mentett tartalmakat. Letöltések ÁLTALÁNOS Elérhető integrációk kezelése @@ -1339,11 +1341,13 @@ GB Váltás Kikapcsolva - Csatlakoztasd saját médiaszerveredet a személyes könyvtárad böngészéséhez és lejátszásához. - Adj meg egy szerver URL-t fent, ha szeretnéd, hogy a Nuvio megjelenítse a privát könyvtáradat. - Nincsenek csatlakoztatott személyes könyvtárak. - Szerver URL - Hozzáadás + Hozd el a könyvtáradat a Nuvióba a használt kiegészítőkkel. + Adj hozzá egy kiegészítőt a könyvtárad felépítéséhez. + Szabd személyre a Nuviót. + Kiegészítő URL-je + Kiegészítő hozzáadása + Itt még nincs semmi + A jelenlegi kiegészítőid még nem jelenítenek meg itt semmit. 28,12 18,35 2020-01-01 @@ -1397,7 +1401,7 @@ Érték megjelenítése Kártya TV-stílusú fekvő kártya - Személyes médiaforrások csatlakoztatása és a saját könyvtáradhoz való hozzáférés kezelése. + Add hozzá és kezeld a Nuvióval használt kiegészítőket. Felhő könyvtár A csatlakoztatott fiókjaidban már meglévő fájlok böngészése és lejátszása. Feloldás ezzel diff --git a/composeApp/src/commonMain/composeResources/values-id/strings.xml b/composeApp/src/commonMain/composeResources/values-id/strings.xml index 35a923d9f..0c79d268d 100644 --- a/composeApp/src/commonMain/composeResources/values-id/strings.xml +++ b/composeApp/src/commonMain/composeResources/values-id/strings.xml @@ -31,6 +31,13 @@ Tidak Tersedia Konfigurasi addon Hapus addon + Bawa pustaka Anda ke Nuvio dengan addon yang Anda gunakan. + Tambahkan addon untuk mulai membangun pustaka Anda. + Jadikan Nuvio milik Anda. + URL Addon + Tambah Addon + Belum ada apa pun di sini + Addon yang Anda gunakan belum memiliki apa pun untuk ditampilkan di sini. Tambahkan URL manifes untuk mulai memuat katalog, metadata, streaming, atau subtitle ke Nuvio. Belum ada addon yang terpasang. Masukkan URL addon. @@ -412,7 +419,9 @@ Unduh versi terbaru Periksa pembaruan Kelola addon dan sumber penemuan. + Kelola addon dan pilih apa yang tampil di Nuvio. Kelola film dan episode yang telah diunduh. + Kelola yang Anda simpan di perangkat ini. Unduhan UMUM Kelola integrasi yang tersedia @@ -597,6 +606,7 @@ BERANDA SUMBER Pasang, hapus, perbarui, dan urutkan sumber konten Anda. + Tambahkan dan kelola addon yang Anda gunakan dengan Nuvio. Pasang repositori scraper JavaScript dan uji penyedia secara internal. Sesuaikan tata letak beranda, visibilitas konten, dan poster Pengaturan untuk layar detail dan episode. diff --git a/composeApp/src/commonMain/composeResources/values-in/strings.xml b/composeApp/src/commonMain/composeResources/values-in/strings.xml index e442e971d..b56c75ce8 100644 --- a/composeApp/src/commonMain/composeResources/values-in/strings.xml +++ b/composeApp/src/commonMain/composeResources/values-in/strings.xml @@ -31,6 +31,13 @@ Tidak Tersedia Konfigurasi addon Hapus addon + Bawa pustaka Anda ke Nuvio dengan addon yang Anda gunakan. + Tambahkan addon untuk mulai membangun pustaka Anda. + Jadikan Nuvio milik Anda. + URL Addon + Tambah Addon + Belum ada apa pun di sini + Addon yang Anda gunakan belum memiliki apa pun untuk ditampilkan di sini. Tambahkan URL manifes untuk mulai memuat katalog, metadata, streaming, atau subtitle ke Nuvio. Belum ada addon yang terpasang. Masukkan URL addon. @@ -412,7 +419,9 @@ Unduh versi terbaru Periksa pembaruan Kelola addon dan sumber penemuan. + Kelola addon dan pilih apa yang tampil di Nuvio. Kelola film dan episode yang telah diunduh. + Kelola yang Anda simpan di perangkat ini. Unduhan UMUM Kelola integrasi yang tersedia @@ -597,6 +606,7 @@ BERANDA SUMBER Pasang, hapus, perbarui, dan urutkan sumber konten Anda. + Tambahkan dan kelola addon yang Anda gunakan dengan Nuvio. Pasang repositori scraper JavaScript dan uji penyedia secara internal. Sesuaikan tata letak beranda, visibilitas konten, dan poster Pengaturan untuk layar detail dan episode. diff --git a/composeApp/src/commonMain/composeResources/values-it/strings.xml b/composeApp/src/commonMain/composeResources/values-it/strings.xml index 035580c22..a4cfd0c42 100644 --- a/composeApp/src/commonMain/composeResources/values-it/strings.xml +++ b/composeApp/src/commonMain/composeResources/values-it/strings.xml @@ -27,6 +27,13 @@ Non Disponibile Configura addon Cancella addon + Porta la tua libreria su Nuvio con gli addon che usi. + Aggiungi un addon per iniziare a creare la tua libreria. + Fai tuo Nuvio. + URL addon + Aggiungi addon + Non c’è ancora niente qui + Gli addon che usi non hanno ancora nulla da mostrare qui. Aggiungi un manifest URL per iniziare a caricare cataloghi , metadata, flussi o sottotitoli dentro Nuvio. Nessun addon installato ancora. Inserisci l'URL dell'addon. @@ -245,7 +252,9 @@ Controlla se ci sono nuove versioni dell'app. Verifica aggiornamenti Gestisci gli addon e le sorgenti di scoperta. + Gestisci gli addon e scegli cosa mostrare in Nuvio. Gestisci i film e gli episodi scaricati. + Gestisci ciò che hai salvato su questo dispositivo. Download GENERALI Collega i servizi TMDB e MDBList. @@ -382,6 +391,7 @@ HOME SORGENTI Installa, rimuovi, aggiorna e ordina le tue sorgenti di contenuto. + Aggiungi e gestisci gli addon che usi con Nuvio. Installa repository di scraper JavaScript e testa i provider internamente. Controlla quali cataloghi appaiono in Home e in quale ordine. Disabilita le sezioni dei dettagli e riordina tutto ciò che sta sotto l'elemento Hero. diff --git a/composeApp/src/commonMain/composeResources/values-ja/strings.xml b/composeApp/src/commonMain/composeResources/values-ja/strings.xml index 45688cd97..885e1c81c 100644 --- a/composeApp/src/commonMain/composeResources/values-ja/strings.xml +++ b/composeApp/src/commonMain/composeResources/values-ja/strings.xml @@ -32,11 +32,13 @@ 利用不可 アドオンを設定 アドオンを削除 - 独自のメディアサーバーを接続して、個人ライブラリを参照・再生します。 - Nuvioに個人ライブラリを表示させたい場合は、上にサーバーURLを追加してください。 - 接続された個人ライブラリはありません。 - サーバーURL - 追加 + いつものアドオンで、ライブラリをNuvioにまとめましょう。 + アドオンを追加して、ライブラリを作り始めましょう。 + Nuvioを自分好みに。 + アドオンURL + アドオンを追加 + まだ何もありません + 現在のアドオンには、ここに表示できるものがまだありません。 マニフェストURLを追加して、カタログ・メタデータ・ストリーム・字幕をNuvioに読み込みましょう。 アドオンがありません アドオンのURLを入力してください。 @@ -442,7 +444,9 @@ 最新リリースをダウンロード アップデートを確認 アドオンと探索ソースを管理 + アドオンを管理して、Nuvioに表示する内容を選べます。 ダウンロードした映画とエピソードを管理 + このデバイスに保存した項目を管理します。 ダウンロード 一般 利用可能な連携を管理 @@ -638,7 +642,7 @@ ホーム ソース コンテンツソースのインストール・削除・更新・並べ替え - 個人メディアソースを接続し、自分専用ライブラリへのアクセスを管理します。 + Nuvioで使うアドオンを追加・管理します。 JavaScriptスクレイパーリポジトリのインストールと内部テスト ホームレイアウト・コンテンツ表示・ポスターの動作を調整 詳細画面とエピソード画面の設定 diff --git a/composeApp/src/commonMain/composeResources/values-nb/strings.xml b/composeApp/src/commonMain/composeResources/values-nb/strings.xml index ad7cea8c3..9957c3dbc 100644 --- a/composeApp/src/commonMain/composeResources/values-nb/strings.xml +++ b/composeApp/src/commonMain/composeResources/values-nb/strings.xml @@ -29,6 +29,13 @@ Utilgjengelig Konfigurer tillegg Slett tillegg + Ta med biblioteket ditt til Nuvio med tilleggene du bruker. + Legg til et tillegg for å begynne å bygge biblioteket ditt. + Gjør Nuvio til ditt. + URL til tillegg + Legg til tillegg + Det er ingenting her ennå + Tilleggene du bruker, har ingenting å vise her ennå. Legg til en manifest-URL for å starte lasting av kataloger, metadata, strømmer eller undertekster i Nuvio. Ingen tillegg installert ennå. Skriv inn en tilleggs-URL. @@ -400,7 +407,9 @@ Last ned nyeste versjon Se etter oppdateringer Administrer tillegg og oppdagelseskilder. + Administrer tillegg og velg hva som vises i Nuvio. Administrer nedlastede filmer og episoder. + Administrer det du har lagret på denne enheten. Nedlastinger GENERELT Administrer tilgjengelige integrasjoner @@ -581,6 +590,7 @@ HJEM KILDER Installer, fjern, oppdater og sorter innholdskilder. + Legg til og administrer tilleggene du bruker med Nuvio. Installer JavaScript-scraper-repositories og test providere internt. Juster hjemmeoppsett, synlighet og plakat-oppførsel. Innstillinger for detalj- og episodeskjermer. diff --git a/composeApp/src/commonMain/composeResources/values-nl/strings.xml b/composeApp/src/commonMain/composeResources/values-nl/strings.xml index 59504af97..c2fdc267f 100644 --- a/composeApp/src/commonMain/composeResources/values-nl/strings.xml +++ b/composeApp/src/commonMain/composeResources/values-nl/strings.xml @@ -32,11 +32,13 @@ Niet beschikbaar Addon configureren Addon verwijderen - Verbind je eigen mediaserver om je persoonlijke bibliotheek te bekijken en af te spelen. - Voeg hierboven een server-URL toe als je wilt dat Nuvio je privébibliotheek toont. - Geen persoonlijke bibliotheken verbonden. - Server-URL - Toevoegen + Breng je bibliotheek naar Nuvio met de add-ons die je gebruikt. + Voeg een add-on toe om je bibliotheek op te bouwen. + Maak Nuvio helemaal van jou. + Add-on-URL + Add-on toevoegen + Hier is nog niets + Je huidige add-ons hebben hier nog niets om te tonen. Voeg een manifest-URL toe om catalogi, metadata, streams of ondertitels in Nuvio te laden. Nog geen addons geïnstalleerd. Voer een addon-URL in. @@ -442,7 +444,9 @@ Download de laatste versie Controleren op updates Beheer addons en ontdekkingsbronnen. + Beheer add-ons en kies wat er in Nuvio verschijnt. Beheer je gedownloade films en afleveringen. + Beheer wat je op dit apparaat hebt opgeslagen. Downloads ALGEMEEN Beheer beschikbare integraties @@ -656,7 +660,7 @@ HOME BRONNEN Installeer, verwijder, vernieuw en sorteer je contentbronnen. - Verbind persoonlijke mediabronnen en beheer toegang tot je eigen bibliotheek. + Voeg de add-ons toe die je met Nuvio gebruikt en beheer ze. Installeer JavaScript-scraperrepository's en test providers intern. Pas startschermindeling, zichtbaarheid van inhoud en postergedrag aan Instellingen voor de detail- en afleveringsschermen. diff --git a/composeApp/src/commonMain/composeResources/values-pl/strings.xml b/composeApp/src/commonMain/composeResources/values-pl/strings.xml index c500dfcf3..473e6a21a 100644 --- a/composeApp/src/commonMain/composeResources/values-pl/strings.xml +++ b/composeApp/src/commonMain/composeResources/values-pl/strings.xml @@ -31,6 +31,13 @@ Niedostępny Konfiguruj dodatek Usuń dodatek + Przenieś swoją bibliotekę do Nuvio za pomocą używanych dodatków. + Dodaj dodatek, aby zacząć tworzyć swoją bibliotekę. + Dostosuj Nuvio do siebie. + URL dodatku + Dodaj dodatek + Na razie nic tu nie ma + Używane dodatki nie mają jeszcze nic do wyświetlenia. Dodaj URL manifestu, aby zacząć ładować katalogi, metadane, strumienie lub napisy do Nuvio. Brak zainstalowanych dodatków. Wprowadź URL dodatku. @@ -412,7 +419,9 @@ Sprawdź dostępność nowych wersji aplikacji. Sprawdź aktualizacje Zarządzaj dodatkami i źródłami odkrywania. + Zarządzaj dodatkami i wybierz, co ma być widoczne w Nuvio. Zarządzaj pobranymi filmami i odcinkami. + Zarządzaj zawartością zapisaną na tym urządzeniu. Pobrane OGÓLNE Połącz usługi TMDB i MDBList. @@ -597,6 +606,7 @@ EKRAN GŁÓWNY ŹRÓDŁA Instaluj, usuwaj, odświeżaj i sortuj źródła treści. + Dodawaj używane w Nuvio dodatki i zarządzaj nimi. Instaluj repozytoria scraperów JavaScript i testuj dostawców wewnętrznie. Kontroluj, które katalogi pojawiają się na ekranie głównym i w jakiej kolejności. Wyłącz sekcje szczegółów i zmień kolejność wszystkiego poniżej Hero. diff --git a/composeApp/src/commonMain/composeResources/values-pt-rBR/strings.xml b/composeApp/src/commonMain/composeResources/values-pt-rBR/strings.xml index 49fb2960d..97b5fde26 100644 --- a/composeApp/src/commonMain/composeResources/values-pt-rBR/strings.xml +++ b/composeApp/src/commonMain/composeResources/values-pt-rBR/strings.xml @@ -30,6 +30,13 @@ Indisponível Configurar addon Excluir addon + Leve sua biblioteca para o Nuvio com os addons que você usa. + Adicione um addon para começar a montar sua biblioteca. + Deixe o Nuvio do seu jeito. + URL do addon + Adicionar addon + Ainda não há nada aqui + Os addons que você usa ainda não têm nada para mostrar aqui. Adicione uma URL de manifesto para começar a carregar catálogos, metadados, streams ou legendas no Nuvio. Nenhum addon instalado ainda. Insira uma URL de addon. @@ -401,7 +408,9 @@ Baixar a versão mais recente Verificar atualizações Gerenciar addons e fontes de descoberta. + Gerencie addons e escolha o que aparece no Nuvio. Gerencie seus filmes e episódios baixados. + Gerencie o que você salvou neste dispositivo. Downloads GERAL Gerenciar integrações disponíveis @@ -582,6 +591,7 @@ PÁGINA INICIAL FONTES Instale, remova, atualize e ordene suas fontes de conteúdo. + Adicione e gerencie os addons que você usa com o Nuvio. Instale repositórios de scraper JavaScript e teste provedores internamente. Ajuste layout da página inicial, visibilidade de conteúdo e comportamento de pôsteres Configurações para as telas de detalhes e episódios. diff --git a/composeApp/src/commonMain/composeResources/values-pt/strings.xml b/composeApp/src/commonMain/composeResources/values-pt/strings.xml index 51a4a7041..e6719a5f5 100644 --- a/composeApp/src/commonMain/composeResources/values-pt/strings.xml +++ b/composeApp/src/commonMain/composeResources/values-pt/strings.xml @@ -31,6 +31,13 @@ Indisponível Configurar addon Eliminar addon + Leva a tua biblioteca para o Nuvio com os addons que usas. + Adiciona um addon para começares a criar a tua biblioteca. + Deixa o Nuvio à tua maneira. + URL do addon + Adicionar addon + Ainda não há nada aqui + Os addons que usas ainda não têm nada para mostrar aqui. Adiciona um URL de manifesto para começares a carregar catálogos, metadados, streams ou legendas no Nuvio. Ainda não tens addons instalados. Introduz o URL de um addon. @@ -432,7 +439,9 @@ Transferir a versão mais recente Procurar atualizações Gere addons e fontes de descoberta. + Gere os addons e escolhe o que aparece no Nuvio. Gere os teus filmes e episódios transferidos. + Gere o que guardaste neste dispositivo. Transferências GERAL Gere as integrações disponíveis @@ -627,6 +636,7 @@ PÁGINA INICIAL FONTES Instala, remove, atualiza e ordena as tuas fontes de conteúdo. + Adiciona e gere os addons que usas com o Nuvio. Instala repositórios de scrapers JavaScript e testa fornecedores internamente. Ajusta o esquema da página inicial, a visibilidade do conteúdo e o comportamento dos cartazes. Definições para as páginas de detalhes e de episódios. diff --git a/composeApp/src/commonMain/composeResources/values-ro/strings.xml b/composeApp/src/commonMain/composeResources/values-ro/strings.xml index a26335699..f002ef425 100644 --- a/composeApp/src/commonMain/composeResources/values-ro/strings.xml +++ b/composeApp/src/commonMain/composeResources/values-ro/strings.xml @@ -32,11 +32,13 @@ Indisponibil Configurează extensia Șterge extensia - Conectează-ți propriul server media pentru a naviga și a reda din biblioteca personală. - Adaugă un URL de server mai sus când vrei ca Nuvio să îți afișeze biblioteca privată. - Nicio bibliotecă personală conectată. - URL Server - Adaugă + Folosește extensiile preferate pentru a-ți aduce biblioteca în Nuvio. + Adaugă o extensie pentru a începe să-ți construiești biblioteca. + Personalizează Nuvio. + URL extensie + Adaugă extensie + Nu este nimic aici încă + Extensiile pe care le folosești nu au încă nimic de afișat aici. Adaugă un URL de manifest pentru a începe încărcarea de cataloage, metadate, fluxuri sau subtitrări în Nuvio. Nu există extensii instalate încă. Introdu un URL pentru extensie. @@ -442,7 +444,9 @@ Descarcă cea mai recentă versiune Caută actualizări Gestionează extensiile și sursele de descoperire. + Gestionează extensiile și alege ce apare în Nuvio. Gestionează filmele și episoadele descărcate. + Gestionează ce ai salvat pe acest dispozitiv. Descărcări GENERAL Gestionează integrările disponibile @@ -656,7 +660,7 @@ ECRAN PRINCIPAL SURSE Instalează, elimină, reîmprospătează și sortează sursele de conținut. - Conectează surse media personale și gestionează accesul la propria bibliotecă. + Adaugă și gestionează extensiile pe care le folosești cu Nuvio. Instalează depozite de scraper JavaScript și testează furnizorii intern. Ajustează aspectul ecranului principal, vizibilitatea conținutului și comportamentul posterului Setări pentru ecranele de detalii și de episoade. @@ -1932,4 +1936,4 @@ %d titlu %d titluri - \ No newline at end of file + diff --git a/composeApp/src/commonMain/composeResources/values-sk/strings.xml b/composeApp/src/commonMain/composeResources/values-sk/strings.xml index f554f5310..9f1c75634 100644 --- a/composeApp/src/commonMain/composeResources/values-sk/strings.xml +++ b/composeApp/src/commonMain/composeResources/values-sk/strings.xml @@ -434,7 +434,9 @@ Stiahnuť najnovšie vydanie Skontrolovať aktualizácie Spravovať doplnky a zdroje objavovania. + Spravujte doplnky a vyberte, čo sa má zobrazovať v Nuviu. Spravovať stiahnuté filmy a epizódy. + Spravujte obsah uložený v tomto zariadení. Stiahnuté VŠEOBECNÉ Spravovať dostupné integrácie @@ -1898,15 +1900,17 @@ Chyba torrentu: %1$s Prepnúť - Pripojte vlastný mediálny server na prehliadanie a prehrávanie z vašej osobnej knižnice. - Pridajte URL servera vyššie, ak chcete, aby Nuvio zobrazovalo vašu súkromnú knižnicu. - Nie sú pripojené žiadne osobné knižnice. - URL servera - Pridať + Preneste svoju knižnicu do Nuvia pomocou doplnkov, ktoré používate. + Pridajte doplnok a začnite si vytvárať knižnicu. + Prispôsobte si Nuvio. + URL doplnku + Pridať doplnok + Zatiaľ tu nič nie je + Doplnky, ktoré používate, tu zatiaľ nemajú čo zobraziť. Registráciou súhlasím s Podmienkami Jazyk zariadenia - Pripojte osobné mediálne zdroje a spravujte prístup k vlastnej knižnici. + Pridávajte a spravujte doplnky, ktoré používate s Nuviom. Odosielať časové značky intra a outra Odovzdať vyriešené časové značky intra a outra externému prehrávaču na automatické preskakovanie. Funguje iba v prehrávačoch, ktoré to podporujú; ostatné prehrávače to ignorujú. Pôvodný jazyk diff --git a/composeApp/src/commonMain/composeResources/values-tr/strings.xml b/composeApp/src/commonMain/composeResources/values-tr/strings.xml index 1340bde98..7a6fb8396 100644 --- a/composeApp/src/commonMain/composeResources/values-tr/strings.xml +++ b/composeApp/src/commonMain/composeResources/values-tr/strings.xml @@ -31,6 +31,13 @@ Kullanılamıyor Eklentiyi ayarla Eklentiyi sil + Kullandığın eklentilerle kitaplığını Nuvio'ya taşı. + Kitaplığını oluşturmaya başlamak için bir eklenti ekle. + Nuvio'yu kendine göre ayarla. + Eklenti URL'si + Eklenti ekle + Burada henüz bir şey yok + Kullandığın eklentilerin burada gösterecek bir şeyi yok. Nuvio'ya katalog, meta veri, yayın veya altyazı yüklemeye başlamak için bir manifest URL'si ekle. Henüz eklenti kurulmamış. Bir eklenti URL'si gir. @@ -402,7 +409,9 @@ Uygulamanın yeni sürümü var mı kontrol et. Güncellemeleri kontrol et Eklentileri ve keşif kaynaklarını yönet. + Eklentileri yönet ve Nuvio’da nelerin görüneceğini seç. İndirdiğin film ve bölümleri yönet. + Bu cihaza kaydettiklerini yönet. İndirilenler GENEL TMDB ve MDBList servislerini bağla. @@ -587,6 +596,7 @@ ANA SAYFA KAYNAKLAR İçerik kaynaklarını kur, kaldır, yenile ve sırala. + Nuvio ile kullandığın eklentileri ekle ve yönet. JavaScript scraper depoları kur ve sağlayıcıları içeriden test et. Ana sayfada hangi katalogların hangi sırayla görüneceğini kontrol et. Detay bölümlerini kapat ve öne çıkanların altındaki her şeyi yeniden sırala. diff --git a/composeApp/src/commonMain/composeResources/values-vi/strings.xml b/composeApp/src/commonMain/composeResources/values-vi/strings.xml index 946da7e68..5e80d6433 100644 --- a/composeApp/src/commonMain/composeResources/values-vi/strings.xml +++ b/composeApp/src/commonMain/composeResources/values-vi/strings.xml @@ -32,11 +32,13 @@ Không khả dụng Cấu hình addon Xóa addon - Kết nối máy chủ nội dung của bạn để duyệt và phát thư viện cá nhân. - Thêm URL máy chủ ở trên để Nuvio hiển thị thư viện riêng của bạn. - Chưa kết nối thư viện cá nhân. - URL máy chủ - Thêm + Đưa thư viện của bạn vào Nuvio với các addon bạn sử dụng. + Thêm một addon để bắt đầu xây dựng thư viện của bạn. + Tùy chỉnh Nuvio theo cách của bạn. + URL addon + Thêm addon + Chưa có nội dung nào ở đây + Các addon bạn đang dùng chưa có nội dung để hiển thị ở đây. Thêm URL manifest để tải danh mục, thông tin, nguồn phát hoặc phụ đề vào Nuvio. Chưa cài addon nào. Nhập URL addon. @@ -442,7 +444,9 @@ Tải phiên bản mới nhất. Kiểm tra cập nhật Quản lý addon và các nguồn khám phá nội dung. + Quản lý addon và chọn nội dung hiển thị trong Nuvio. Quản lý phim và tập đã tải xuống. + Quản lý nội dung bạn đã lưu trên thiết bị này. Tải xuống CHUNG Quản lý các dịch vụ tích hợp. @@ -641,7 +645,7 @@ TRANG CHỦ NGUỒN Cài đặt, xóa, làm mới và sắp xếp các nguồn nội dung. - Kết nối nguồn nội dung cá nhân và quản lý thư viện riêng của bạn. + Thêm và quản lý các addon bạn sử dụng với Nuvio. Cài đặt kho Plugin JavaScript và thử nghiệm các dịch vụ nội bộ. Điều chỉnh bố cục Trang chủ, khả năng hiển thị nội dung và cách hiển thị poster. Thiết lập cho trang chi tiết và danh sách tập. @@ -1924,4 +1928,4 @@ Đang khởi động Engine P2P… Không thể khởi động torrent: %1$s Lỗi torrent: %1$s - \ No newline at end of file + diff --git a/composeApp/src/commonMain/composeResources/values/strings.xml b/composeApp/src/commonMain/composeResources/values/strings.xml index e06f8af69..d81ca03d1 100644 --- a/composeApp/src/commonMain/composeResources/values/strings.xml +++ b/composeApp/src/commonMain/composeResources/values/strings.xml @@ -32,11 +32,13 @@ Unavailable Configure addon Delete addon - Connect your own media server to browse and play from your personal library. - Add a server URL above when you want Nuvio to show your private library. - No personal libraries connected. - Server URL - Add + Bring your library into Nuvio with the addons you use. + Add an addon to start building your library. + Make Nuvio yours. + Addon URL + Add Addon + Nothing here yet + Your current addons don’t have anything to show here. Add a manifest URL to start loading catalogs, metadata, streams or subtitles into Nuvio. No addons installed yet. Enter an addon URL. @@ -442,7 +444,9 @@ Download latest release Check for updates Manage addons and discovery sources. + Manage addons and choose what appears in Nuvio. Manage your downloaded movies and episodes. + Manage what you’ve saved on this device. Downloads GENERAL Manage available integrations @@ -687,7 +691,7 @@ HOME SOURCES Install, remove, refresh, and sort your content sources. - Connect personal media sources and manage access to your own library. + Add and manage the addons you use with Nuvio. Install JavaScript scraper repositories and test providers internally. Adjust home layout, content visibility, and poster behavior Settings for the detail and episode screens. diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/core/build/AppFeaturePolicy.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/core/build/AppFeaturePolicy.kt index ec7ed1779..95584b5a1 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/core/build/AppFeaturePolicy.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/core/build/AppFeaturePolicy.kt @@ -9,7 +9,7 @@ expect object AppFeaturePolicy { val pluginsEnabled: Boolean val supportersContributorsPageEnabled: Boolean val accountDeletionEnabled: Boolean - val personalMediaAddonCopyEnabled: Boolean + val storeNarrativeEnabled: Boolean val p2pEnabled: Boolean val trailerPlaybackMode: TrailerPlaybackMode val heroTrailerPlaybackSupported: Boolean diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/core/ui/EmptyState.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/core/ui/EmptyState.kt new file mode 100644 index 000000000..7cce66129 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/core/ui/EmptyState.kt @@ -0,0 +1,83 @@ +package com.nuvio.app.core.ui + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +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.material.icons.Icons +import androidx.compose.material.icons.rounded.SearchOff +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp + +@Composable +fun NuvioEmptyState( + title: String, + message: String? = null, + modifier: Modifier = Modifier, + icon: ImageVector = Icons.Rounded.SearchOff, + iconPainter: Painter? = null, + actionLabel: String? = null, + onActionClick: (() -> Unit)? = null, +) { + Column( + modifier = modifier + .fillMaxWidth() + .padding(32.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(8.dp, Alignment.CenterVertically), + ) { + if (iconPainter != null) { + Icon( + painter = iconPainter, + contentDescription = null, + modifier = Modifier.size(48.dp), + tint = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f), + ) + } else { + Icon( + imageVector = icon, + contentDescription = null, + modifier = Modifier.size(48.dp), + tint = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f), + ) + } + Spacer(modifier = Modifier.height(8.dp)) + Text( + text = title, + style = MaterialTheme.typography.bodyLarge.copy( + fontSize = 16.sp, + fontWeight = FontWeight.SemiBold, + ), + color = MaterialTheme.colorScheme.onSurfaceVariant, + textAlign = TextAlign.Center, + ) + if (!message.isNullOrBlank()) { + Text( + text = message, + style = MaterialTheme.typography.bodySmall.copy(fontSize = 14.sp), + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f), + textAlign = TextAlign.Center, + ) + } + if (actionLabel != null && onActionClick != null) { + Spacer(modifier = Modifier.height(8.dp)) + NuvioPrimaryButton( + text = actionLabel, + onClick = onActionClick, + ) + } + } +} diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/addons/AddonsScreen.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/addons/AddonsScreen.kt index 2d7db4a85..57ddd621f 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/addons/AddonsScreen.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/addons/AddonsScreen.kt @@ -9,6 +9,7 @@ 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.heightIn import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width @@ -47,6 +48,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.nuvio.app.core.build.AppFeaturePolicy import coil3.compose.AsyncImage import com.nuvio.app.core.ui.NuvioIconActionButton +import com.nuvio.app.core.ui.NuvioEmptyState import com.nuvio.app.core.ui.NuvioInfoBadge import com.nuvio.app.core.ui.NuvioInputField import com.nuvio.app.core.ui.NuvioPrimaryButton @@ -94,7 +96,7 @@ internal fun AddonsSettingsPageContent( var formMessage by rememberSaveable { mutableStateOf(null) } var installModalState by remember { mutableStateOf(null) } val enterAddonUrlMessage = stringResource(Res.string.addons_error_enter_url) - val usePersonalMediaCopy = AppFeaturePolicy.personalMediaAddonCopyEnabled + val useStoreNarrative = AppFeaturePolicy.storeNarrativeEnabled val overview = remember(uiState.addons) { uiState.addons.toOverview() } @@ -109,7 +111,7 @@ internal fun AddonsSettingsPageContent( AddAddonCard( addonUrl = addonUrl, formMessage = formMessage, - usePersonalMediaCopy = usePersonalMediaCopy, + useStoreNarrative = useStoreNarrative, onAddonUrlChange = { addonUrl = it formMessage = null @@ -141,7 +143,7 @@ internal fun AddonsSettingsPageContent( SectionHeader(stringResource(Res.string.addons_section_installed)) if (uiState.addons.isEmpty()) { - EmptyStateCard(usePersonalMediaCopy = usePersonalMediaCopy) + EmptyStateCard(useStoreNarrative = useStoreNarrative) } else { val lastIndex = uiState.addons.lastIndex uiState.addons.forEachIndexed { index, addon -> @@ -286,7 +288,7 @@ private fun VerticalSeparator() { private fun AddAddonCard( addonUrl: String, formMessage: String?, - usePersonalMediaCopy: Boolean, + useStoreNarrative: Boolean, onAddonUrlChange: (String) -> Unit, onAddClick: () -> Unit, ) { @@ -294,27 +296,15 @@ private fun AddAddonCard( NuvioInputField( value = addonUrl, onValueChange = onAddonUrlChange, - placeholder = stringResource( - if (usePersonalMediaCopy) { - Res.string.addons_appstore_input_placeholder - } else { - Res.string.addons_input_placeholder - }, - ), + placeholder = stringResource(Res.string.addons_input_placeholder), ) Spacer(modifier = Modifier.height(18.dp)) NuvioPrimaryButton( - text = stringResource( - if (usePersonalMediaCopy) { - Res.string.addons_appstore_install_button - } else { - Res.string.addons_install_button - }, - ), + text = stringResource(Res.string.addons_install_button), enabled = addonUrl.isNotBlank(), onClick = onAddClick, ) - if (usePersonalMediaCopy) { + if (useStoreNarrative) { Spacer(modifier = Modifier.height(14.dp)) Text( text = stringResource(Res.string.addons_appstore_add_description), @@ -355,33 +345,26 @@ private sealed interface AddonInstallModalState { @Composable private fun EmptyStateCard( - usePersonalMediaCopy: Boolean, + useStoreNarrative: Boolean, ) { - NuvioSurfaceCard { - Text( - text = stringResource( - if (usePersonalMediaCopy) { - Res.string.addons_appstore_empty_title - } else { - Res.string.addons_empty_title - }, - ), - style = MaterialTheme.typography.titleLarge, - color = MaterialTheme.colorScheme.onSurface, - ) - Spacer(modifier = Modifier.height(8.dp)) - Text( - text = stringResource( - if (usePersonalMediaCopy) { - Res.string.addons_appstore_empty_subtitle - } else { - Res.string.addons_empty_subtitle - }, - ), - style = MaterialTheme.typography.bodyLarge, - color = MaterialTheme.colorScheme.onSurfaceVariant, - ) - } + NuvioEmptyState( + modifier = Modifier.heightIn(min = 220.dp), + icon = Icons.Rounded.Extension, + title = stringResource( + if (useStoreNarrative) { + Res.string.addons_appstore_empty_title + } else { + Res.string.addons_empty_title + }, + ), + message = stringResource( + if (useStoreNarrative) { + Res.string.addons_appstore_empty_subtitle + } else { + Res.string.addons_empty_subtitle + }, + ), + ) } @Composable diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/catalog/CatalogScreen.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/catalog/CatalogScreen.kt index c11312d5e..362fe6d84 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/catalog/CatalogScreen.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/catalog/CatalogScreen.kt @@ -23,6 +23,7 @@ import androidx.compose.foundation.lazy.grid.items import androidx.compose.foundation.lazy.grid.rememberLazyGridState import androidx.compose.foundation.shape.RoundedCornerShape import com.nuvio.app.core.ui.NuvioLoadingIndicator +import com.nuvio.app.core.ui.NuvioEmptyState import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -162,6 +163,7 @@ fun CatalogScreen( .background(MaterialTheme.colorScheme.background), ) { val columns = remember(maxWidth) { catalogGridColumnsForWidth(maxWidth) } + val viewportHeight = maxHeight Box(modifier = Modifier.fillMaxSize()) { LazyVerticalGrid( @@ -184,6 +186,9 @@ fun CatalogScreen( } else if (uiState.items.isEmpty()) { item(span = { GridItemSpan(maxLineSpan) }) { CatalogEmptyState( + modifier = Modifier.height( + (viewportHeight - 120.dp).coerceAtLeast(280.dp), + ), errorMessage = uiState.errorMessage, networkCondition = networkStatusUiState.condition, onRetry = { @@ -361,6 +366,7 @@ private fun CatalogEmptyState( errorMessage: String?, networkCondition: NetworkCondition, onRetry: (() -> Unit)? = null, + modifier: Modifier = Modifier, ) { if (networkCondition == NetworkCondition.NoInternet || networkCondition == NetworkCondition.ServersUnreachable) { NuvioNetworkOfflineCard( @@ -370,24 +376,11 @@ private fun CatalogEmptyState( return } - Column( - modifier = Modifier - .fillMaxWidth() - .padding(vertical = 48.dp), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(10.dp), - ) { - Text( - text = stringResource(Res.string.catalog_empty_title), - style = MaterialTheme.typography.titleLarge, - color = MaterialTheme.colorScheme.onBackground, - ) - Text( - text = errorMessage ?: stringResource(Res.string.catalog_empty_message), - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant, - ) - } + NuvioEmptyState( + modifier = modifier, + title = stringResource(Res.string.catalog_empty_title), + message = errorMessage ?: stringResource(Res.string.catalog_empty_message), + ) } @Composable diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/downloads/DownloadsScreen.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/downloads/DownloadsScreen.kt index ef6a1eb2c..d96fae0bf 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/downloads/DownloadsScreen.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/downloads/DownloadsScreen.kt @@ -2,7 +2,6 @@ package com.nuvio.app.features.downloads import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer @@ -14,7 +13,7 @@ import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.foundation.lazy.items import androidx.compose.material.icons.Icons import androidx.compose.material.icons.rounded.Delete -import androidx.compose.material.icons.rounded.Folder +import androidx.compose.material.icons.rounded.Download import androidx.compose.material.icons.rounded.Pause import androidx.compose.material.icons.rounded.PlayArrow import androidx.compose.material.icons.rounded.Refresh @@ -38,8 +37,8 @@ import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.nuvio.app.core.i18n.localizedByteUnit import com.nuvio.app.core.ui.NuvioScreen +import com.nuvio.app.core.ui.NuvioEmptyState import com.nuvio.app.core.ui.NuvioScreenHeader -import com.nuvio.app.core.ui.NuvioToastController import nuvio.composeapp.generated.resources.* import org.jetbrains.compose.resources.stringResource @@ -57,8 +56,6 @@ fun DownloadsScreen( }.collectAsStateWithLifecycle() var selectedShowId by rememberSaveable(initialShowId) { mutableStateOf(initialShowId) } - val openDownloadsDirectoryFailedText = stringResource(Res.string.downloads_open_directory_failed) - val completedEpisodes = remember(uiState.items) { uiState.completedItems .filter { it.isEpisode } @@ -86,20 +83,6 @@ fun DownloadsScreen( onBack() } }, - actions = { - IconButton( - onClick = { - if (!DownloadsPlatformDownloader.openDownloadsDirectory()) { - NuvioToastController.show(openDownloadsDirectoryFailedText) - } - }, - ) { - Icon( - imageVector = Icons.Rounded.Folder, - contentDescription = stringResource(Res.string.downloads_open_directory), - ) - } - }, ) } @@ -228,18 +211,11 @@ private fun LazyListScope.downloadsRootContent( if (uiState.items.isEmpty()) { item { - Box( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 20.dp, vertical = 40.dp), - contentAlignment = Alignment.Center, - ) { - Text( - text = stringResource(Res.string.downloads_empty_title), - style = MaterialTheme.typography.titleMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant, - ) - } + NuvioEmptyState( + modifier = Modifier.fillParentMaxHeight(), + icon = Icons.Rounded.Download, + title = stringResource(Res.string.downloads_empty_title), + ) } } } @@ -264,18 +240,11 @@ private fun LazyListScope.downloadsShowContent( if (seasons.isEmpty()) { item { - Box( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 20.dp, vertical = 40.dp), - contentAlignment = Alignment.Center, - ) { - Text( - text = stringResource(Res.string.downloads_empty_episodes), - style = MaterialTheme.typography.titleMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant, - ) - } + NuvioEmptyState( + modifier = Modifier.fillParentMaxHeight(), + icon = Icons.Rounded.Download, + title = stringResource(Res.string.downloads_empty_episodes), + ) } return } diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/home/HomeScreen.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/home/HomeScreen.kt index 0cb700758..d2642216b 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/home/HomeScreen.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/home/HomeScreen.kt @@ -15,6 +15,7 @@ import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.nuvio.app.core.auth.AuthRepository import com.nuvio.app.core.auth.AuthState +import com.nuvio.app.core.build.AppFeaturePolicy import com.nuvio.app.core.network.NetworkCondition import com.nuvio.app.core.network.NetworkStatusRepository import com.nuvio.app.core.ui.LocalNuvioBottomNavigationOverlayPadding @@ -36,7 +37,6 @@ import com.nuvio.app.features.details.seriesPrimaryAction import com.nuvio.app.features.home.components.HomeCatalogRowSection import com.nuvio.app.features.home.components.HomeContinueWatchingSection import com.nuvio.app.features.home.components.HomeEmptyStateCard -import com.nuvio.app.features.home.components.HomeHeroReservedSpace import com.nuvio.app.features.home.components.HomeHeroSection import com.nuvio.app.features.home.components.HomeSkeletonHero import com.nuvio.app.features.home.components.HomeSkeletonRow @@ -107,7 +107,6 @@ fun HomeScreen( onFirstCatalogRendered: (() -> Unit)? = null, ) { LaunchedEffect(Unit) { - AddonRepository.initialize() CollectionRepository.initialize() ContinueWatchingPreferencesRepository.ensureLoaded() WatchedRepository.ensureLoaded() @@ -118,7 +117,10 @@ fun HomeScreen( } } - val addonsUiState by AddonRepository.uiState.collectAsStateWithLifecycle() + val addonsUiState by remember { + AddonRepository.initialize() + AddonRepository.uiState + }.collectAsStateWithLifecycle() val homeUiState by HomeRepository.uiState.collectAsStateWithLifecycle() val homeSettingsUiState by remember { HomeCatalogSettingsRepository.snapshot() @@ -440,7 +442,6 @@ fun HomeScreen( } LaunchedEffect(catalogRefreshKey) { - if (catalogRefreshKey.isEmpty()) return@LaunchedEffect HomeCatalogSettingsRepository.syncCatalogs(enabledAddons) HomeRepository.refresh(enabledAddons) } @@ -655,11 +656,15 @@ fun HomeScreen( } val hasActiveAddons = enabledAddons.any { it.manifest != null } - val showHeroSlot = homeSettingsUiState.heroEnabled + val hasCurrentHeroSource = enabledAddons.any { it.manifest != null || it.isRefreshing } || + collections.any { it.folders.isNotEmpty() } val isResolvingHeroSources = enabledAddons.any { it.isRefreshing } || homeUiState.isLoading - val showHeroSkeleton = showHeroSlot && + val showHeroSkeleton = homeSettingsUiState.heroEnabled && + enabledAddons.isNotEmpty() && homeUiState.heroItems.isEmpty() && isResolvingHeroSources + val showHeroSlot = homeSettingsUiState.heroEnabled && hasCurrentHeroSource && + (homeUiState.heroItems.isNotEmpty() || showHeroSkeleton) var firstCatalogReported by remember { mutableStateOf(false) } LaunchedEffect(homeUiState.sections.firstOrNull()?.key, onFirstCatalogRendered) { @@ -749,14 +754,14 @@ fun HomeScreen( ) { if (showHeroSlot) { item { - when { - showHeroSkeleton -> HomeSkeletonHero( + if (showHeroSkeleton) { + HomeSkeletonHero( modifier = Modifier, viewportHeight = maxHeight, mobileBelowSectionHeightHint = mobileHeroBelowSectionHeightHint, ) - - homeUiState.heroItems.isNotEmpty() -> HomeHeroSection( + } else { + HomeHeroSection( items = homeUiState.heroItems, modifier = Modifier, viewportHeight = maxHeight, @@ -764,12 +769,6 @@ fun HomeScreen( listState = homeListState, onItemClick = onPosterClick, ) - - else -> HomeHeroReservedSpace( - modifier = Modifier, - viewportHeight = maxHeight, - mobileBelowSectionHeightHint = mobileHeroBelowSectionHeightHint, - ) } } } @@ -792,10 +791,31 @@ fun HomeScreen( } } item { + val emptyStateModifier = if ( + continueWatchingPreferences.isVisible && continueWatchingItems.isNotEmpty() + ) { + Modifier.padding(horizontal = 16.dp) + } else { + Modifier + .fillParentMaxHeight() + .padding(horizontal = 16.dp) + } HomeEmptyStateCard( - modifier = Modifier.padding(horizontal = 16.dp), - title = stringResource(Res.string.compose_search_empty_no_active_addons_title), - message = stringResource(Res.string.home_empty_no_active_addons_message), + modifier = emptyStateModifier, + title = stringResource( + if (AppFeaturePolicy.storeNarrativeEnabled) { + Res.string.addons_empty_title + } else { + Res.string.compose_search_empty_no_active_addons_title + }, + ), + message = stringResource( + if (AppFeaturePolicy.storeNarrativeEnabled) { + Res.string.addons_appstore_empty_subtitle + } else { + Res.string.home_empty_no_active_addons_message + }, + ), ) } } @@ -839,10 +859,22 @@ fun HomeScreen( ) } else { HomeEmptyStateCard( - modifier = Modifier.padding(horizontal = 16.dp), - title = stringResource(Res.string.home_empty_no_rows_title), - message = homeUiState.errorMessage - ?: stringResource(Res.string.home_empty_no_rows_message), + modifier = Modifier + .fillParentMaxHeight() + .padding(horizontal = 16.dp), + title = stringResource( + if (AppFeaturePolicy.storeNarrativeEnabled) { + Res.string.store_empty_unavailable_title + } else { + Res.string.home_empty_no_rows_title + }, + ), + message = if (AppFeaturePolicy.storeNarrativeEnabled) { + stringResource(Res.string.store_empty_unavailable_message) + } else { + homeUiState.errorMessage + ?: stringResource(Res.string.home_empty_no_rows_message) + }, ) } } diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/home/components/HomeStateCards.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/home/components/HomeStateCards.kt index ad8db1533..262a9eefc 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/home/components/HomeStateCards.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/home/components/HomeStateCards.kt @@ -1,41 +1,25 @@ package com.nuvio.app.features.home.components -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.height -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp -import com.nuvio.app.core.ui.NuvioPrimaryButton -import com.nuvio.app.core.ui.NuvioSurfaceCard +import androidx.compose.ui.graphics.painter.Painter +import com.nuvio.app.core.ui.NuvioEmptyState @Composable fun HomeEmptyStateCard( title: String, message: String, modifier: Modifier = Modifier, + iconPainter: Painter? = null, actionLabel: String? = null, onActionClick: (() -> Unit)? = null, ) { - NuvioSurfaceCard(modifier = modifier) { - Text( - text = title, - style = MaterialTheme.typography.titleLarge, - color = MaterialTheme.colorScheme.onSurface, - ) - Spacer(modifier = Modifier.height(8.dp)) - Text( - text = message, - style = MaterialTheme.typography.bodyLarge, - color = MaterialTheme.colorScheme.onSurfaceVariant, - ) - if (actionLabel != null && onActionClick != null) { - Spacer(modifier = Modifier.height(16.dp)) - NuvioPrimaryButton( - text = actionLabel, - onClick = onActionClick, - ) - } - } + NuvioEmptyState( + modifier = modifier, + title = title, + message = message, + iconPainter = iconPainter, + actionLabel = actionLabel, + onActionClick = onActionClick, + ) } diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/library/LibraryScreen.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/library/LibraryScreen.kt index 9e29c5776..4f06bff85 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/library/LibraryScreen.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/library/LibraryScreen.kt @@ -84,6 +84,7 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.launch import nuvio.composeapp.generated.resources.* +import org.jetbrains.compose.resources.painterResource import org.jetbrains.compose.resources.stringResource @Composable @@ -268,7 +269,10 @@ fun LibraryScreen( ) } else { HomeEmptyStateCard( - modifier = Modifier.padding(horizontal = 16.dp), + modifier = Modifier + .fillParentMaxHeight(0.55f) + .padding(horizontal = 16.dp), + iconPainter = painterResource(Res.drawable.sidebar_library), title = if (isTraktSource) { stringResource(Res.string.library_trakt_load_failed) } else { @@ -292,7 +296,10 @@ fun LibraryScreen( ) } else { HomeEmptyStateCard( - modifier = Modifier.padding(horizontal = 16.dp), + modifier = Modifier + .fillParentMaxHeight(0.55f) + .padding(horizontal = 16.dp), + iconPainter = painterResource(Res.drawable.sidebar_library), title = if (isTraktSource) { stringResource(Res.string.library_trakt_empty_title) } else { @@ -345,7 +352,10 @@ private fun LazyListScope.cloudLibraryContent( !uiState.isEnabled -> { item { HomeEmptyStateCard( - modifier = Modifier.padding(horizontal = 16.dp), + modifier = Modifier + .fillParentMaxHeight(0.55f) + .padding(horizontal = 16.dp), + iconPainter = painterResource(Res.drawable.sidebar_library), title = stringResource(Res.string.cloud_library_disabled_title), message = stringResource(Res.string.cloud_library_disabled_message), actionLabel = stringResource(Res.string.cloud_library_disabled_action), @@ -357,7 +367,10 @@ private fun LazyListScope.cloudLibraryContent( !uiState.hasConnectedProvider -> { item { HomeEmptyStateCard( - modifier = Modifier.padding(horizontal = 16.dp), + modifier = Modifier + .fillParentMaxHeight(0.55f) + .padding(horizontal = 16.dp), + iconPainter = painterResource(Res.drawable.sidebar_library), title = stringResource(Res.string.cloud_library_connect_title), message = stringResource(Res.string.cloud_library_connect_message), actionLabel = stringResource(Res.string.cloud_library_connect_action), @@ -407,6 +420,7 @@ private fun LazyListScope.cloudLibraryContent( item(key = "cloud-error-${providerState.providerId}") { HomeEmptyStateCard( modifier = Modifier.padding(horizontal = 16.dp), + iconPainter = painterResource(Res.drawable.sidebar_library), title = stringResource(Res.string.cloud_library_load_failed, providerState.providerName), message = providerState.errorMessage.orEmpty(), actionLabel = stringResource(Res.string.action_retry), @@ -420,7 +434,10 @@ private fun LazyListScope.cloudLibraryContent( } else if (filteredItems.isEmpty()) { item { HomeEmptyStateCard( - modifier = Modifier.padding(horizontal = 16.dp), + modifier = Modifier + .fillParentMaxHeight(0.5f) + .padding(horizontal = 16.dp), + iconPainter = painterResource(Res.drawable.sidebar_library), title = stringResource(Res.string.cloud_library_empty_title), message = stringResource(Res.string.cloud_library_empty_message), actionLabel = stringResource(Res.string.action_retry), diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/search/SearchDiscoverContent.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/search/SearchDiscoverContent.kt index ed58b7f28..d3221b88c 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/search/SearchDiscoverContent.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/search/SearchDiscoverContent.kt @@ -31,6 +31,7 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import coil3.compose.AsyncImage +import com.nuvio.app.core.build.AppFeaturePolicy import com.nuvio.app.core.network.NetworkCondition import com.nuvio.app.core.format.formatReleaseDateForDisplay import com.nuvio.app.core.ui.NuvioDropdownChip @@ -94,7 +95,9 @@ internal fun LazyListScope.discoverContent( items(2) { DiscoverSkeletonRow( columns = columns, - modifier = Modifier.padding(horizontal = 16.dp), + modifier = Modifier + .fillParentMaxHeight(0.72f) + .padding(horizontal = 16.dp), ) } } @@ -353,13 +356,37 @@ private fun DiscoverEmptyStateCard( when (reason) { DiscoverEmptyStateReason.NoActiveAddons -> { - title = stringResource(Res.string.compose_search_empty_no_active_addons_title) - message = stringResource(Res.string.discover_empty_no_active_addons_message) + title = stringResource( + if (AppFeaturePolicy.storeNarrativeEnabled) { + Res.string.addons_empty_title + } else { + Res.string.compose_search_empty_no_active_addons_title + }, + ) + message = stringResource( + if (AppFeaturePolicy.storeNarrativeEnabled) { + Res.string.addons_appstore_empty_subtitle + } else { + Res.string.discover_empty_no_active_addons_message + }, + ) } DiscoverEmptyStateReason.NoDiscoverCatalogs -> { - title = stringResource(Res.string.discover_empty_no_catalogs_title) - message = stringResource(Res.string.discover_empty_no_catalogs_message) + title = stringResource( + if (AppFeaturePolicy.storeNarrativeEnabled) { + Res.string.store_empty_unavailable_title + } else { + Res.string.discover_empty_no_catalogs_title + }, + ) + message = stringResource( + if (AppFeaturePolicy.storeNarrativeEnabled) { + Res.string.store_empty_unavailable_message + } else { + Res.string.discover_empty_no_catalogs_message + }, + ) } DiscoverEmptyStateReason.RequestFailed -> { diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/search/SearchScreen.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/search/SearchScreen.kt index 4520befe5..0448e3689 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/search/SearchScreen.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/search/SearchScreen.kt @@ -40,6 +40,7 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.nuvio.app.core.build.AppFeaturePolicy import com.nuvio.app.core.network.NetworkCondition import com.nuvio.app.core.network.NetworkStatusRepository import com.nuvio.app.core.ui.NuvioInputField @@ -64,6 +65,8 @@ import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.map import nuvio.composeapp.generated.resources.Res +import nuvio.composeapp.generated.resources.addons_appstore_empty_subtitle +import nuvio.composeapp.generated.resources.addons_empty_title import nuvio.composeapp.generated.resources.compose_nav_search import nuvio.composeapp.generated.resources.compose_search_clear import nuvio.composeapp.generated.resources.compose_search_discover_title @@ -78,6 +81,8 @@ import nuvio.composeapp.generated.resources.compose_search_empty_no_search_catal import nuvio.composeapp.generated.resources.compose_search_placeholder import nuvio.composeapp.generated.resources.compose_search_recent_searches import nuvio.composeapp.generated.resources.compose_search_remove_recent_search +import nuvio.composeapp.generated.resources.store_empty_unavailable_message +import nuvio.composeapp.generated.resources.store_empty_unavailable_title import org.jetbrains.compose.resources.stringResource @Composable @@ -317,7 +322,9 @@ fun SearchScreen( isWaitingForSearch -> { items(2) { HomeSkeletonRow( - modifier = Modifier.padding(horizontal = homeSectionPadding), + modifier = Modifier + .fillParentMaxHeight() + .padding(horizontal = homeSectionPadding), showHeaderAccent = !homeCatalogSettingsUiState.hideCatalogUnderline, ) } @@ -413,13 +420,37 @@ private fun SearchEmptyStateCard( when (reason) { SearchEmptyStateReason.NoActiveAddons -> { - title = stringResource(Res.string.compose_search_empty_no_active_addons_title) - message = stringResource(Res.string.compose_search_empty_no_active_addons_message) + title = stringResource( + if (AppFeaturePolicy.storeNarrativeEnabled) { + Res.string.addons_empty_title + } else { + Res.string.compose_search_empty_no_active_addons_title + }, + ) + message = stringResource( + if (AppFeaturePolicy.storeNarrativeEnabled) { + Res.string.addons_appstore_empty_subtitle + } else { + Res.string.compose_search_empty_no_active_addons_message + }, + ) } SearchEmptyStateReason.NoSearchCatalogs -> { - title = stringResource(Res.string.compose_search_empty_no_search_catalogs_title) - message = stringResource(Res.string.compose_search_empty_no_search_catalogs_message) + title = stringResource( + if (AppFeaturePolicy.storeNarrativeEnabled) { + Res.string.store_empty_unavailable_title + } else { + Res.string.compose_search_empty_no_search_catalogs_title + }, + ) + message = stringResource( + if (AppFeaturePolicy.storeNarrativeEnabled) { + Res.string.store_empty_unavailable_message + } else { + Res.string.compose_search_empty_no_search_catalogs_message + }, + ) } SearchEmptyStateReason.RequestFailed -> { diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/ContentDiscoverySettingsPage.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/ContentDiscoverySettingsPage.kt index e4d923875..51188e97d 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/ContentDiscoverySettingsPage.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/ContentDiscoverySettingsPage.kt @@ -19,14 +19,20 @@ internal fun LazyListScope.contentDiscoveryContent( ) { item { SettingsSection( - title = stringResource(Res.string.settings_content_discovery_section_sources), + title = stringResource( + if (AppFeaturePolicy.storeNarrativeEnabled) { + Res.string.compose_settings_page_addons + } else { + Res.string.settings_content_discovery_section_sources + }, + ), isTablet = isTablet, ) { SettingsGroup(isTablet = isTablet) { SettingsNavigationRow( title = stringResource(Res.string.compose_settings_page_addons), description = stringResource( - if (AppFeaturePolicy.personalMediaAddonCopyEnabled) { + if (AppFeaturePolicy.storeNarrativeEnabled) { Res.string.settings_content_discovery_addons_description_appstore } else { Res.string.settings_content_discovery_addons_description diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/HomescreenSettingsPage.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/HomescreenSettingsPage.kt index 254d49e18..43ce15194 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/HomescreenSettingsPage.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/HomescreenSettingsPage.kt @@ -31,6 +31,7 @@ import androidx.compose.ui.hapticfeedback.HapticFeedbackType import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp +import com.nuvio.app.core.build.AppFeaturePolicy import com.nuvio.app.core.ui.NuvioActionLabel import com.nuvio.app.core.ui.NuvioToastController import com.nuvio.app.features.home.HomeCatalogSettingsItem @@ -58,6 +59,8 @@ import nuvio.composeapp.generated.resources.settings_homescreen_show_hero import nuvio.composeapp.generated.resources.settings_homescreen_show_hero_description import nuvio.composeapp.generated.resources.settings_homescreen_summary import nuvio.composeapp.generated.resources.settings_homescreen_summary_hint +import nuvio.composeapp.generated.resources.store_empty_unavailable_message +import nuvio.composeapp.generated.resources.store_empty_unavailable_title import org.jetbrains.compose.resources.stringResource import sh.calvin.reorderable.ReorderableCollectionItemScope import sh.calvin.reorderable.ReorderableItem @@ -133,9 +136,23 @@ internal fun LazyListScope.homescreenSettingsContent( item { if (items.isEmpty()) { HomeEmptyStateCard( - modifier = Modifier.fillMaxWidth(), - title = stringResource(Res.string.settings_homescreen_empty_title), - message = stringResource(Res.string.settings_homescreen_empty_message), + modifier = Modifier + .fillMaxWidth() + .heightIn(min = 280.dp), + title = stringResource( + if (AppFeaturePolicy.storeNarrativeEnabled) { + Res.string.store_empty_unavailable_title + } else { + Res.string.settings_homescreen_empty_title + }, + ), + message = stringResource( + if (AppFeaturePolicy.storeNarrativeEnabled) { + Res.string.store_empty_unavailable_message + } else { + Res.string.settings_homescreen_empty_message + }, + ), ) } else { val catalogCount = items.count { !it.isCollection } diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/SettingsRootPage.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/SettingsRootPage.kt index 1ed8b5d86..fb460046d 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/SettingsRootPage.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/SettingsRootPage.kt @@ -20,6 +20,7 @@ import androidx.compose.material3.Text import androidx.compose.ui.Modifier import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp +import com.nuvio.app.core.build.AppFeaturePolicy import com.nuvio.app.core.build.AppVersionConfig import nuvio.composeapp.generated.resources.Res import nuvio.composeapp.generated.resources.compose_about_made_with @@ -37,7 +38,9 @@ import nuvio.composeapp.generated.resources.compose_settings_root_appearance_des import nuvio.composeapp.generated.resources.compose_settings_root_check_updates_description import nuvio.composeapp.generated.resources.compose_settings_root_check_updates_title import nuvio.composeapp.generated.resources.compose_settings_root_content_discovery_description +import nuvio.composeapp.generated.resources.compose_settings_root_content_discovery_description_appstore import nuvio.composeapp.generated.resources.compose_settings_root_downloads_description +import nuvio.composeapp.generated.resources.compose_settings_root_downloads_description_appstore import nuvio.composeapp.generated.resources.compose_settings_root_downloads_title import nuvio.composeapp.generated.resources.compose_settings_root_general_section import nuvio.composeapp.generated.resources.compose_settings_root_integrations_description @@ -130,7 +133,13 @@ internal fun LazyListScope.settingsRootContent( SettingsGroupDivider(isTablet = isTablet) SettingsNavigationRow( title = stringResource(Res.string.compose_settings_page_content_discovery), - description = stringResource(Res.string.compose_settings_root_content_discovery_description), + description = stringResource( + if (AppFeaturePolicy.storeNarrativeEnabled) { + Res.string.compose_settings_root_content_discovery_description_appstore + } else { + Res.string.compose_settings_root_content_discovery_description + }, + ), icon = Icons.Rounded.Extension, isTablet = isTablet, onClick = onContentDiscoveryClick, @@ -138,7 +147,13 @@ internal fun LazyListScope.settingsRootContent( SettingsGroupDivider(isTablet = isTablet) SettingsNavigationRow( title = stringResource(Res.string.compose_settings_root_downloads_title), - description = stringResource(Res.string.compose_settings_root_downloads_description), + description = stringResource( + if (AppFeaturePolicy.storeNarrativeEnabled) { + Res.string.compose_settings_root_downloads_description_appstore + } else { + Res.string.compose_settings_root_downloads_description + }, + ), icon = Icons.Rounded.CloudDownload, isTablet = isTablet, onClick = onDownloadsClick, diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/SettingsScreen.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/SettingsScreen.kt index 6f42f03ef..e21a8032f 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/SettingsScreen.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/SettingsScreen.kt @@ -567,7 +567,7 @@ private fun MobileSettingsScreen( pluginsEnabled = AppFeaturePolicy.pluginsEnabled, supportersContributorsPageEnabled = AppFeaturePolicy.supportersContributorsPageEnabled, accountDeletionEnabled = AppFeaturePolicy.accountDeletionEnabled, - personalMediaAddonCopyEnabled = AppFeaturePolicy.personalMediaAddonCopyEnabled, + storeNarrativeEnabled = AppFeaturePolicy.storeNarrativeEnabled, liquidGlassNativeTabBarSupported = liquidGlassNativeTabBarSupported, switchProfileAvailable = onSwitchProfile != null, checkForUpdatesAvailable = onCheckForUpdatesClick != null, @@ -955,7 +955,7 @@ private fun TabletSettingsScreen( pluginsEnabled = AppFeaturePolicy.pluginsEnabled, supportersContributorsPageEnabled = AppFeaturePolicy.supportersContributorsPageEnabled, accountDeletionEnabled = AppFeaturePolicy.accountDeletionEnabled, - personalMediaAddonCopyEnabled = AppFeaturePolicy.personalMediaAddonCopyEnabled, + storeNarrativeEnabled = AppFeaturePolicy.storeNarrativeEnabled, liquidGlassNativeTabBarSupported = liquidGlassNativeTabBarSupported, switchProfileAvailable = onSwitchProfile != null, checkForUpdatesAvailable = onCheckForUpdatesClick != null, diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/SettingsSearch.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/SettingsSearch.kt index 419d5567f..47206f871 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/SettingsSearch.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/SettingsSearch.kt @@ -8,6 +8,7 @@ import androidx.compose.animation.fadeIn import androidx.compose.animation.slideInVertically import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.material.icons.Icons @@ -43,6 +44,7 @@ import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import com.nuvio.app.core.ui.NuvioTokens +import com.nuvio.app.core.ui.NuvioEmptyState import com.nuvio.app.core.ui.nuvio import com.nuvio.app.isIos import nuvio.composeapp.generated.resources.* @@ -81,7 +83,7 @@ internal fun settingsSearchEntries( pluginsEnabled: Boolean, supportersContributorsPageEnabled: Boolean, accountDeletionEnabled: Boolean, - personalMediaAddonCopyEnabled: Boolean, + storeNarrativeEnabled: Boolean, liquidGlassNativeTabBarSupported: Boolean, switchProfileAvailable: Boolean, checkForUpdatesAvailable: Boolean, @@ -225,13 +227,25 @@ internal fun settingsSearchEntries( page = SettingsPage.ContentDiscovery, key = "content-discovery", title = contentDiscoveryPage, - description = stringResource(Res.string.compose_settings_root_content_discovery_description), + description = stringResource( + if (storeNarrativeEnabled) { + Res.string.compose_settings_root_content_discovery_description_appstore + } else { + Res.string.compose_settings_root_content_discovery_description + }, + ), icon = Icons.Rounded.Extension, ) add( key = "downloads", title = downloadsPage, - description = stringResource(Res.string.compose_settings_root_downloads_description), + description = stringResource( + if (storeNarrativeEnabled) { + Res.string.compose_settings_root_downloads_description_appstore + } else { + Res.string.compose_settings_root_downloads_description + }, + ), category = generalCategory, icon = Icons.Rounded.CloudDownload, target = SettingsSearchTarget.Downloads, @@ -448,7 +462,7 @@ internal fun settingsSearchEntries( key = "addons", title = addonsPage, description = stringResource( - if (personalMediaAddonCopyEnabled) { + if (storeNarrativeEnabled) { Res.string.settings_content_discovery_addons_description_appstore } else { Res.string.settings_content_discovery_addons_description @@ -1106,25 +1120,14 @@ private fun SettingsSearchField( @Composable private fun SettingsSearchEmptyState(isTablet: Boolean) { - val tokens = MaterialTheme.nuvio SettingsSection( title = stringResource(Res.string.settings_search_results_section), isTablet = isTablet, ) { - SettingsGroup(isTablet = isTablet) { - Column( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = if (isTablet) 20.dp else 16.dp, vertical = 18.dp), - ) { - Text( - text = stringResource(Res.string.settings_search_empty), - style = MaterialTheme.typography.bodyLarge, - color = tokens.colors.textPrimary, - fontWeight = FontWeight.Medium, - ) - } - } + NuvioEmptyState( + modifier = Modifier.heightIn(min = 280.dp), + title = stringResource(Res.string.settings_search_empty), + ) } } diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/streams/StreamsScreen.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/streams/StreamsScreen.kt index 0d3e16772..e94adf57b 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/streams/StreamsScreen.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/streams/StreamsScreen.kt @@ -41,7 +41,6 @@ import androidx.compose.material.icons.automirrored.rounded.OpenInNew import androidx.compose.material.icons.rounded.ContentCopy import androidx.compose.material.icons.rounded.Download import androidx.compose.material.icons.rounded.Refresh -import androidx.compose.material.icons.rounded.SearchOff import com.nuvio.app.core.ui.NuvioLoadingIndicator import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme @@ -75,6 +74,7 @@ import com.nuvio.app.core.build.AppFeaturePolicy import com.nuvio.app.core.ui.NuvioBackButton import com.nuvio.app.core.ui.NuvioBottomSheetActionRow import com.nuvio.app.core.ui.NuvioBottomSheetDivider +import com.nuvio.app.core.ui.NuvioEmptyState import com.nuvio.app.core.ui.NuvioModalBottomSheet import com.nuvio.app.core.ui.NuvioToastController import com.nuvio.app.core.ui.dismissNuvioBottomSheet @@ -892,7 +892,10 @@ internal fun StreamList( !hasAnyStreams && !uiState.isAnyLoading -> { item { - EmptyStateBlock(reason = uiState.emptyStateReason) + EmptyStateBlock( + reason = uiState.emptyStateReason, + modifier = Modifier.fillParentMaxHeight(), + ) } } @@ -1249,13 +1252,37 @@ private fun EmptyStateBlock( when (reason) { StreamsEmptyStateReason.NoAddonsInstalled -> { - title = stringResource(Res.string.compose_search_empty_no_active_addons_title) - message = stringResource(Res.string.streams_empty_no_addons_message) + title = stringResource( + if (AppFeaturePolicy.storeNarrativeEnabled) { + Res.string.addons_empty_title + } else { + Res.string.compose_search_empty_no_active_addons_title + }, + ) + message = stringResource( + if (AppFeaturePolicy.storeNarrativeEnabled) { + Res.string.addons_appstore_empty_subtitle + } else { + Res.string.streams_empty_no_addons_message + }, + ) } StreamsEmptyStateReason.NoCompatibleAddons -> { - title = stringResource(Res.string.streams_empty_no_stream_addon_title) - message = stringResource(Res.string.streams_empty_no_stream_addon_message) + title = stringResource( + if (AppFeaturePolicy.storeNarrativeEnabled) { + Res.string.store_empty_unavailable_title + } else { + Res.string.streams_empty_no_stream_addon_title + }, + ) + message = stringResource( + if (AppFeaturePolicy.storeNarrativeEnabled) { + Res.string.store_empty_unavailable_message + } else { + Res.string.streams_empty_no_stream_addon_message + }, + ) } StreamsEmptyStateReason.StreamFetchFailed -> { @@ -1269,35 +1296,11 @@ private fun EmptyStateBlock( } } - Column( - modifier = modifier - .fillMaxWidth() - .padding(32.dp), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(8.dp), - ) { - Icon( - imageVector = Icons.Rounded.SearchOff, - contentDescription = null, - modifier = Modifier.size(48.dp), - tint = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f), - ) - Spacer(modifier = Modifier.height(8.dp)) - Text( - text = title, - style = MaterialTheme.typography.bodyLarge.copy( - fontSize = 16.sp, - fontWeight = FontWeight.SemiBold, - ), - color = MaterialTheme.colorScheme.onSurfaceVariant, - ) - Text( - text = message, - style = MaterialTheme.typography.bodySmall.copy(fontSize = 14.sp), - color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f), - textAlign = TextAlign.Center, - ) - } + NuvioEmptyState( + modifier = modifier, + title = title, + message = message, + ) } @Composable diff --git a/composeApp/src/desktopMain/kotlin/com/nuvio/app/core/build/AppFeaturePolicy.desktop.kt b/composeApp/src/desktopMain/kotlin/com/nuvio/app/core/build/AppFeaturePolicy.desktop.kt index bc70435f0..aa8fb74f4 100644 --- a/composeApp/src/desktopMain/kotlin/com/nuvio/app/core/build/AppFeaturePolicy.desktop.kt +++ b/composeApp/src/desktopMain/kotlin/com/nuvio/app/core/build/AppFeaturePolicy.desktop.kt @@ -4,7 +4,7 @@ actual object AppFeaturePolicy { actual val pluginsEnabled: Boolean = false actual val supportersContributorsPageEnabled: Boolean = true actual val accountDeletionEnabled: Boolean = false - actual val personalMediaAddonCopyEnabled: Boolean = false + actual val storeNarrativeEnabled: Boolean = false actual val p2pEnabled: Boolean = false actual val trailerPlaybackMode: TrailerPlaybackMode = TrailerPlaybackMode.EXTERNAL actual val heroTrailerPlaybackSupported: Boolean = false diff --git a/composeApp/src/iosAppStore/kotlin/com/nuvio/app/core/build/AppFeaturePolicy.ios.kt b/composeApp/src/iosAppStore/kotlin/com/nuvio/app/core/build/AppFeaturePolicy.ios.kt index 6cc679aac..4267d3245 100644 --- a/composeApp/src/iosAppStore/kotlin/com/nuvio/app/core/build/AppFeaturePolicy.ios.kt +++ b/composeApp/src/iosAppStore/kotlin/com/nuvio/app/core/build/AppFeaturePolicy.ios.kt @@ -4,7 +4,7 @@ actual object AppFeaturePolicy { actual val pluginsEnabled: Boolean = false actual val supportersContributorsPageEnabled: Boolean = false actual val accountDeletionEnabled: Boolean = true - actual val personalMediaAddonCopyEnabled: Boolean = true + actual val storeNarrativeEnabled: Boolean = true actual val p2pEnabled: Boolean = false actual val trailerPlaybackMode: TrailerPlaybackMode = TrailerPlaybackMode.EXTERNAL actual val heroTrailerPlaybackSupported: Boolean = false diff --git a/composeApp/src/iosFull/kotlin/com/nuvio/app/core/build/AppFeaturePolicy.ios.kt b/composeApp/src/iosFull/kotlin/com/nuvio/app/core/build/AppFeaturePolicy.ios.kt index b86fa9f4b..b8f0d8371 100644 --- a/composeApp/src/iosFull/kotlin/com/nuvio/app/core/build/AppFeaturePolicy.ios.kt +++ b/composeApp/src/iosFull/kotlin/com/nuvio/app/core/build/AppFeaturePolicy.ios.kt @@ -4,7 +4,7 @@ actual object AppFeaturePolicy { actual val pluginsEnabled: Boolean = true actual val supportersContributorsPageEnabled: Boolean = true actual val accountDeletionEnabled: Boolean = false - actual val personalMediaAddonCopyEnabled: Boolean = false + actual val storeNarrativeEnabled: Boolean = false actual val p2pEnabled: Boolean = false actual val trailerPlaybackMode: TrailerPlaybackMode = TrailerPlaybackMode.IN_APP actual val heroTrailerPlaybackSupported: Boolean = false diff --git a/iosApp/iosApp.xcodeproj/project.pbxproj b/iosApp/iosApp.xcodeproj/project.pbxproj index 2a2c1e595..b0fd8c3fe 100644 --- a/iosApp/iosApp.xcodeproj/project.pbxproj +++ b/iosApp/iosApp.xcodeproj/project.pbxproj @@ -246,7 +246,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "if [ \"YES\" = \"$OVERRIDE_KOTLIN_BUILD_IDE_SUPPORTED\" ]; then\n echo \"Skipping Gradle build task invocation due to OVERRIDE_KOTLIN_BUILD_IDE_SUPPORTED environment variable set to \\\"YES\\\"\"\n exit 0\nfi\nif [ -z \"$JAVA_HOME\" ] && [ -x \"/opt/homebrew/opt/openjdk@17/libexec/openjdk.jdk/Contents/Home/bin/java\" ]; then\n export JAVA_HOME=\"/opt/homebrew/opt/openjdk@17/libexec/openjdk.jdk/Contents/Home\"\nfi\nif [ -n \"$JAVA_HOME\" ]; then\n export PATH=\"$JAVA_HOME/bin:/opt/homebrew/bin:$PATH\"\nelse\n export PATH=\"/opt/homebrew/bin:$PATH\"\nfi\nif [ -z \"$GRADLE_OPTS\" ]; then\n export GRADLE_OPTS=\"-Xmx12288M -Dfile.encoding=UTF-8 -XX:MaxMetaspaceSize=2048m\"\nfi\nif [ -z \"$KOTLIN_DAEMON_JVMARGS\" ]; then\n export KOTLIN_DAEMON_JVMARGS=\"-Xmx8192M\"\nfi\ncd \"$SRCROOT/..\"\n./gradlew :composeApp:embedAndSignAppleFrameworkForXcode\n"; + shellScript = "if [ \"YES\" = \"$OVERRIDE_KOTLIN_BUILD_IDE_SUPPORTED\" ]; then\n echo \"Skipping Gradle build task invocation due to OVERRIDE_KOTLIN_BUILD_IDE_SUPPORTED environment variable set to \\\"YES\\\"\"\n exit 0\nfi\nexport JAVA_HOME=\"/Applications/Android Studio.app/Contents/jbr/Contents/Home\"\nexport PATH=\"$JAVA_HOME/bin:/opt/homebrew/bin:$PATH\"\nif [ -z \"$GRADLE_OPTS\" ]; then\n export GRADLE_OPTS=\"-Xmx12288M -Dfile.encoding=UTF-8 -XX:MaxMetaspaceSize=2048m\"\nfi\nif [ -z \"$KOTLIN_DAEMON_JVMARGS\" ]; then\n export KOTLIN_DAEMON_JVMARGS=\"-Xmx8192M\"\nfi\ncd \"$SRCROOT/..\"\n./gradlew :composeApp:embedAndSignAppleFrameworkForXcode\n"; }; /* End PBXShellScriptBuildPhase section */ From e25e12df09584eb5544863a00d0e41cb130c5111 Mon Sep 17 00:00:00 2001 From: tapframe <85391825+tapframe@users.noreply.github.com> Date: Sun, 12 Jul 2026 19:30:31 +0530 Subject: [PATCH 3/6] fix: prevent settings sync reset regression --- .../app/core/sync/ProfileSettingsSync.kt | 316 ++---------------- 1 file changed, 32 insertions(+), 284 deletions(-) diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/core/sync/ProfileSettingsSync.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/core/sync/ProfileSettingsSync.kt index 11fe09158..ad0774205 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/core/sync/ProfileSettingsSync.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/core/sync/ProfileSettingsSync.kt @@ -29,7 +29,6 @@ import com.nuvio.app.features.tmdb.TmdbSettingsStorage import com.nuvio.app.features.tmdb.TmdbSettingsRepository import com.nuvio.app.features.trakt.TraktCommentsStorage import com.nuvio.app.features.trakt.TraktCommentsSettings -import com.nuvio.app.features.trakt.ProfileSettingsWatchSourceOutbox import com.nuvio.app.features.trakt.TraktSettingsStorage import com.nuvio.app.features.trakt.TraktSettingsRepository import com.nuvio.app.features.watchprogress.ContinueWatchingPreferencesStorage @@ -37,21 +36,16 @@ import com.nuvio.app.features.watchprogress.ContinueWatchingPreferencesRepositor import io.github.jan.supabase.postgrest.postgrest import io.github.jan.supabase.postgrest.rpc import kotlin.concurrent.Volatile -import kotlinx.atomicfu.locks.SynchronizedObject -import kotlinx.atomicfu.locks.synchronized -import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.Job import kotlinx.coroutines.SupervisorJob -import kotlinx.coroutines.delay import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.drop import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock @@ -67,22 +61,10 @@ import kotlinx.serialization.json.put private const val PUSH_DEBOUNCE_MS = 1500L -private data class ObservedProfileSettingsChange( - val signature: String, - val accountId: String?, -) - -private data class SkippedProfileSettingsPush( - val signature: String, - val accountId: String?, - val profileId: Int, -) - object ProfileSettingsSync { private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default) private val log = Logger.withTag("ProfileSettingsSync") private val syncMutex = Mutex() - private val observeLock = SynchronizedObject() private val json = Json { ignoreUnknownKeys = true encodeDefaults = true @@ -95,62 +77,33 @@ object ProfileSettingsSync { private var isServerSyncInFlight: Boolean = false @Volatile - private var skipNextPush: SkippedProfileSettingsPush? = null - - @Volatile - private var pushEnabledAccountId: String? = null - - @Volatile - private var pendingLocalPush: SkippedProfileSettingsPush? = null + private var skipNextPushSignature: String? = null private var observeJob: Job? = null - private var pendingPushRetryJob: Job? = null - fun startObserving() = synchronized(observeLock) { - if (observeJob?.isActive == true) return@synchronized + fun startObserving() { + if (observeJob?.isActive == true) return ensureRepositoriesLoaded() observeLocalChangesAndPush() } fun clearAccountState() { - synchronized(observeLock) { - observeJob?.cancel() - observeJob = null - } - skipNextPush = null - pushEnabledAccountId = null - pendingLocalPush = null - pendingPushRetryJob?.cancel() - pendingPushRetryJob = null + observeJob?.cancel() + observeJob = null + skipNextPushSignature = null } suspend fun pull(profileId: Int): Boolean { - startObserving() - val accountId = currentCloudAccountId() ?: return false + ensureRepositoriesLoaded() return syncMutex.withLock { - if (!isCurrentSyncTarget(profileId = profileId, accountId = accountId)) { + if (ProfileRepository.activeProfileId != profileId) { log.d { "pull(profileId=$profileId) — skipped because profile is no longer active" } return@withLock false } isServerSyncInFlight = true try { - pushEnabledAccountId = accountId - hydrateDurableWatchSourcePush(profileId = profileId, accountId = accountId) - if ( - !pushCurrentStateLocked( - profileId = profileId, - accountId = accountId, - forceCurrentState = false, - ) - ) { - schedulePendingPushRetry() - return@withLock false - } - val observedSignatureAtStart = currentObservedStateSignature() val localBlob = exportSettingsBlob() - if (!isCurrentSyncTarget(profileId = profileId, accountId = accountId)) { - throw CancellationException("Profile settings pull target changed") - } + if (ProfileRepository.activeProfileId != profileId) return@withLock false val localSignature = buildSignature(localBlob) val params = buildJsonObject { @@ -158,105 +111,41 @@ object ProfileSettingsSync { put("p_platform", MOBILE_SYNC_PLATFORM) } val result = SupabaseProvider.client.postgrest.rpc("sync_pull_profile_settings_blob", params) - if (!isCurrentSyncTarget(profileId = profileId, accountId = accountId)) { - throw CancellationException("Profile settings pull target changed") - } + if (ProfileRepository.activeProfileId != profileId) return@withLock false val response = result.decodeList().firstOrNull() val remoteJson = response?.settingsJson - val pendingDuringPull = pendingLocalPush?.let { pending -> - pending.accountId == accountId && pending.profileId == profileId - } == true - val durableSourceChangeDuringPull = - ProfileSettingsWatchSourceOutbox.pendingFor(accountId, profileId) != null - if ( - pendingDuringPull || - durableSourceChangeDuringPull || - currentObservedStateSignature() != observedSignatureAtStart - ) { - if ( - !pushCurrentStateLocked( - profileId = profileId, - accountId = accountId, - forceCurrentState = true, - ) - ) { - schedulePendingPushRetry() - } - return@withLock false - } - if (remoteJson == null) { log.i { "pull(profileId=$profileId) — no remote settings blob found" } - if (localSignature != defaultSignature()) { - pushToRemoteLocked(profileId, localBlob, accountId) - } - pushEnabledAccountId = accountId return@withLock false } - val remoteBlob = try { - json.decodeFromJsonElement(MobileProfileSettingsBlob.serializer(), remoteJson) - } catch (error: Throwable) { - log.e(error) { "pull(profileId=$profileId) — failed to decode remote settings blob" } - throw error - } - - var restoredPendingSourceAfterRemoteApply = false isApplyingRemoteBlob = true try { + val remoteBlob = runCatching { + json.decodeFromJsonElement(MobileProfileSettingsBlob.serializer(), remoteJson) + }.getOrElse { error -> + log.e(error) { "pull(profileId=$profileId) — failed to decode remote settings blob" } + return@withLock false + } val remoteSignature = buildSignature(remoteBlob) if (remoteSignature == localSignature) { log.d { "pull(profileId=$profileId) — remote matches local" } - pushEnabledAccountId = accountId return@withLock false } - if (!isCurrentSyncTarget(profileId = profileId, accountId = accountId)) { - throw CancellationException("Profile settings pull target changed") - } + if (ProfileRepository.activeProfileId != profileId) return@withLock false applyRemoteBlob(remoteBlob) - ProfileSettingsWatchSourceOutbox.pendingFor(accountId, profileId)?.let { pendingSource -> - if (TraktSettingsRepository.uiState.value.watchProgressSource != pendingSource.source) { - TraktSettingsRepository.setWatchProgressSource(pendingSource.source, profileId) - } - restoredPendingSourceAfterRemoteApply = true - } - skipNextPush = SkippedProfileSettingsPush( - signature = currentObservedStateSignature(), - accountId = currentCloudAccountId(), - profileId = profileId, - ) + skipNextPushSignature = currentObservedStateSignature() } finally { isApplyingRemoteBlob = false } - if (restoredPendingSourceAfterRemoteApply) { - pendingLocalPush = SkippedProfileSettingsPush( - signature = currentObservedStateSignature(), - accountId = accountId, - profileId = profileId, - ) - if ( - !pushCurrentStateLocked( - profileId = profileId, - accountId = accountId, - forceCurrentState = true, - ) - ) { - schedulePendingPushRetry() - } - return@withLock false - } - log.i { "pull(profileId=$profileId) — applied remote settings blob" } - pushEnabledAccountId = accountId true - } catch (error: CancellationException) { - throw error } catch (error: Exception) { log.e(error) { "pull(profileId=$profileId) — FAILED" } - throw error + false } finally { isServerSyncInFlight = false } @@ -265,22 +154,16 @@ object ProfileSettingsSync { suspend fun pushCurrentProfileToRemote(): Boolean { ensureRepositoriesLoaded() - val accountId = currentCloudAccountId() ?: return false return syncMutex.withLock { - try { + runCatching { val profileId = ProfileRepository.activeProfileId - if (!isCurrentSyncTarget(profileId = profileId, accountId = accountId)) return@withLock false - pushCurrentStateLocked( - profileId = profileId, - accountId = accountId, - forceCurrentState = true, - ) - } catch (error: CancellationException) { - throw error - } catch (error: Throwable) { + val blob = exportSettingsBlob() + if (ProfileRepository.activeProfileId != profileId) return@runCatching false + pushToRemoteLocked(profileId, blob) + true + }.onFailure { error -> log.e(error) { "pushCurrentProfileToRemote() — FAILED" } - false - } + }.getOrDefault(false) } } @@ -306,84 +189,24 @@ object ProfileSettingsSync { ) observeJob = scope.launch { - combine(signatureFlows) { - ObservedProfileSettingsChange( - signature = currentObservedStateSignature(), - accountId = currentCloudAccountId(), - ) - } + combine(signatureFlows) { currentObservedStateSignature() } .drop(1) .distinctUntilChanged() - .onEach { change -> - val authState = AuthRepository.state.value - if (authState !is AuthState.Authenticated || authState.isAnonymous) return@onEach - if (change.accountId == null || change.accountId != authState.userId) return@onEach - val observedChange = SkippedProfileSettingsPush( - signature = change.signature, - accountId = change.accountId, - profileId = ProfileRepository.activeProfileId, - ) - if (skipNextPush != observedChange) { - pendingLocalPush = observedChange - schedulePendingPushRetry() - } - } .debounce(PUSH_DEBOUNCE_MS) - .collect { change -> + .collect { signature -> val authState = AuthRepository.state.value if (authState !is AuthState.Authenticated || authState.isAnonymous) return@collect - if (change.accountId == null || change.accountId != authState.userId) return@collect - val profileId = ProfileRepository.activeProfileId - val observedChange = SkippedProfileSettingsPush( - signature = change.signature, - accountId = change.accountId, - profileId = profileId, - ) - if (skipNextPush == observedChange) { - skipNextPush = null - if (pendingLocalPush == observedChange) { - pendingLocalPush = null - } + if (isApplyingRemoteBlob || isServerSyncInFlight) return@collect + if (signature == skipNextPushSignature) { + skipNextPushSignature = null return@collect } - pendingLocalPush = observedChange - if (pushEnabledAccountId != change.accountId) return@collect - if (isApplyingRemoteBlob || isServerSyncInFlight) return@collect pushCurrentProfileToRemote() } } } - private fun schedulePendingPushRetry() { - if (pendingPushRetryJob?.isActive == true) return - pendingPushRetryJob = scope.launch { - var retryDelayMs = 5_000L - while (pendingLocalPush != null) { - delay(retryDelayMs) - val pending = pendingLocalPush ?: break - if ( - pending.accountId == currentCloudAccountId() && - pending.profileId == ProfileRepository.activeProfileId && - pushEnabledAccountId == pending.accountId && - !isApplyingRemoteBlob && - !isServerSyncInFlight && - pushCurrentProfileToRemote() - ) { - break - } - retryDelayMs = (retryDelayMs * 2L).coerceAtMost(60_000L) - } - } - } - - private suspend fun pushToRemoteLocked( - profileId: Int, - blob: MobileProfileSettingsBlob, - accountId: String, - ) { - if (!isCurrentSyncTarget(profileId = profileId, accountId = accountId)) { - throw CancellationException("Profile settings push target changed") - } + private suspend fun pushToRemoteLocked(profileId: Int, blob: MobileProfileSettingsBlob) { val params = buildJsonObject { put("p_profile_id", profileId) put("p_platform", MOBILE_SYNC_PLATFORM) @@ -391,74 +214,9 @@ object ProfileSettingsSync { putSyncOriginClientId() } SupabaseProvider.client.postgrest.rpc("sync_push_profile_settings_blob", params) - if (!isCurrentSyncTarget(profileId = profileId, accountId = accountId)) { - throw CancellationException("Profile settings push target changed") - } log.d { "pushToRemoteLocked(profileId=$profileId) — success" } } - private fun hydrateDurableWatchSourcePush(profileId: Int, accountId: String) { - val durableChange = ProfileSettingsWatchSourceOutbox.pendingFor(accountId, profileId) ?: return - if (TraktSettingsRepository.uiState.value.watchProgressSource != durableChange.source) { - TraktSettingsRepository.setWatchProgressSource(durableChange.source, profileId) - } - pendingLocalPush = SkippedProfileSettingsPush( - signature = currentObservedStateSignature(), - accountId = accountId, - profileId = profileId, - ) - schedulePendingPushRetry() - } - - private suspend fun pushCurrentStateLocked( - profileId: Int, - accountId: String, - forceCurrentState: Boolean, - ): Boolean { - val durableChange = ProfileSettingsWatchSourceOutbox.pendingFor(accountId, profileId) - val inMemoryChange = pendingLocalPush?.takeIf { pending -> - pending.accountId == accountId && pending.profileId == profileId - } - if (!forceCurrentState && durableChange == null && inMemoryChange == null) return true - - val signature = currentObservedStateSignature() - pushToRemoteLocked(profileId, exportSettingsBlob(), accountId) - if (currentObservedStateSignature() != signature) { - pendingLocalPush = SkippedProfileSettingsPush( - signature = currentObservedStateSignature(), - accountId = accountId, - profileId = profileId, - ) - schedulePendingPushRetry() - return false - } - - val pushedChange = SkippedProfileSettingsPush( - signature = signature, - accountId = accountId, - profileId = profileId, - ) - if (pendingLocalPush == pushedChange) { - pendingLocalPush = null - } - if ( - durableChange != null && - TraktSettingsRepository.uiState.value.watchProgressSource == durableChange.source - ) { - ProfileSettingsWatchSourceOutbox.clearIfMatches(durableChange) - } - - val durablePushRemains = ProfileSettingsWatchSourceOutbox.pendingFor(accountId, profileId) != null - val memoryPushRemains = pendingLocalPush?.let { pending -> - pending.accountId == accountId && pending.profileId == profileId - } == true - if (durablePushRemains || memoryPushRemains) { - schedulePendingPushRetry() - return false - } - return true - } - private fun exportSettingsBlob(): MobileProfileSettingsBlob { ensureRepositoriesLoaded() return MobileProfileSettingsBlob( @@ -547,9 +305,6 @@ object ProfileSettingsSync { private fun buildSignature(blob: MobileProfileSettingsBlob): String = json.encodeToString(MobileProfileSettingsBlob.serializer(), blob) - private fun defaultSignature(): String = - buildSignature(MobileProfileSettingsBlob()) - private fun currentObservedStateSignature(): String = listOf( "theme=${ThemeSettingsRepository.selectedTheme.value.name}", "amoled=${ThemeSettingsRepository.amoledEnabled.value}", @@ -569,13 +324,6 @@ object ProfileSettingsSync { "episode_release_alerts=${EpisodeReleaseNotificationsRepository.uiState.value.isEnabled}", ).joinToString(separator = "||") - private fun currentCloudAccountId(): String? = - (AuthRepository.state.value as? AuthState.Authenticated) - ?.takeUnless { it.isAnonymous } - ?.userId - - private fun isCurrentSyncTarget(profileId: Int, accountId: String): Boolean = - ProfileRepository.activeProfileId == profileId && currentCloudAccountId() == accountId } @Serializable From fc87a44adc23064611b2b73ac9a951772f8fda98 Mon Sep 17 00:00:00 2001 From: tapframe <85391825+tapframe@users.noreply.github.com> Date: Sun, 12 Jul 2026 19:36:11 +0530 Subject: [PATCH 4/6] revert: restore previous empty state UI --- .../core/build/AppFeaturePolicy.android.kt | 2 +- .../core/build/AppFeaturePolicy.android.kt | 2 +- .../nuvio/app/core/build/AppFeaturePolicy.kt | 2 +- .../com/nuvio/app/core/ui/EmptyState.kt | 83 ------------------- .../nuvio/app/features/addons/AddonsScreen.kt | 73 +++++++++------- .../app/features/catalog/CatalogScreen.kt | 29 ++++--- .../app/features/downloads/DownloadsScreen.kt | 55 +++++++++--- .../com/nuvio/app/features/home/HomeScreen.kt | 78 +++++------------ .../home/components/HomeStateCards.kt | 38 ++++++--- .../app/features/library/LibraryScreen.kt | 27 ++---- .../features/search/SearchDiscoverContent.kt | 37 ++------- .../nuvio/app/features/search/SearchScreen.kt | 41 ++------- .../settings/ContentDiscoverySettingsPage.kt | 10 +-- .../settings/HomescreenSettingsPage.kt | 23 +---- .../app/features/settings/SettingsRootPage.kt | 19 +---- .../app/features/settings/SettingsScreen.kt | 4 +- .../app/features/settings/SettingsSearch.kt | 41 +++++---- .../app/features/streams/StreamsScreen.kt | 73 ++++++++-------- .../core/build/AppFeaturePolicy.desktop.kt | 2 +- .../app/core/build/AppFeaturePolicy.ios.kt | 2 +- .../app/core/build/AppFeaturePolicy.ios.kt | 2 +- iosApp/iosApp.xcodeproj/project.pbxproj | 2 +- 22 files changed, 241 insertions(+), 404 deletions(-) delete mode 100644 composeApp/src/commonMain/kotlin/com/nuvio/app/core/ui/EmptyState.kt diff --git a/composeApp/src/androidFull/kotlin/com/nuvio/app/core/build/AppFeaturePolicy.android.kt b/composeApp/src/androidFull/kotlin/com/nuvio/app/core/build/AppFeaturePolicy.android.kt index f4de81fc1..364def91d 100644 --- a/composeApp/src/androidFull/kotlin/com/nuvio/app/core/build/AppFeaturePolicy.android.kt +++ b/composeApp/src/androidFull/kotlin/com/nuvio/app/core/build/AppFeaturePolicy.android.kt @@ -4,7 +4,7 @@ actual object AppFeaturePolicy { actual val pluginsEnabled: Boolean = true actual val supportersContributorsPageEnabled: Boolean = true actual val accountDeletionEnabled: Boolean = false - actual val storeNarrativeEnabled: Boolean = false + actual val personalMediaAddonCopyEnabled: Boolean = false actual val p2pEnabled: Boolean = true actual val trailerPlaybackMode: TrailerPlaybackMode = TrailerPlaybackMode.IN_APP actual val heroTrailerPlaybackSupported: Boolean = true diff --git a/composeApp/src/androidPlaystore/kotlin/com/nuvio/app/core/build/AppFeaturePolicy.android.kt b/composeApp/src/androidPlaystore/kotlin/com/nuvio/app/core/build/AppFeaturePolicy.android.kt index 3db44aa5e..f0ee56ff0 100644 --- a/composeApp/src/androidPlaystore/kotlin/com/nuvio/app/core/build/AppFeaturePolicy.android.kt +++ b/composeApp/src/androidPlaystore/kotlin/com/nuvio/app/core/build/AppFeaturePolicy.android.kt @@ -4,7 +4,7 @@ actual object AppFeaturePolicy { actual val pluginsEnabled: Boolean = false actual val supportersContributorsPageEnabled: Boolean = true actual val accountDeletionEnabled: Boolean = false - actual val storeNarrativeEnabled: Boolean = true + actual val personalMediaAddonCopyEnabled: Boolean = false actual val p2pEnabled: Boolean = true actual val trailerPlaybackMode: TrailerPlaybackMode = TrailerPlaybackMode.EXTERNAL actual val heroTrailerPlaybackSupported: Boolean = false diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/core/build/AppFeaturePolicy.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/core/build/AppFeaturePolicy.kt index 95584b5a1..ec7ed1779 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/core/build/AppFeaturePolicy.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/core/build/AppFeaturePolicy.kt @@ -9,7 +9,7 @@ expect object AppFeaturePolicy { val pluginsEnabled: Boolean val supportersContributorsPageEnabled: Boolean val accountDeletionEnabled: Boolean - val storeNarrativeEnabled: Boolean + val personalMediaAddonCopyEnabled: Boolean val p2pEnabled: Boolean val trailerPlaybackMode: TrailerPlaybackMode val heroTrailerPlaybackSupported: Boolean diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/core/ui/EmptyState.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/core/ui/EmptyState.kt deleted file mode 100644 index 7cce66129..000000000 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/core/ui/EmptyState.kt +++ /dev/null @@ -1,83 +0,0 @@ -package com.nuvio.app.core.ui - -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -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.material.icons.Icons -import androidx.compose.material.icons.rounded.SearchOff -import androidx.compose.material3.Icon -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.painter.Painter -import androidx.compose.ui.graphics.vector.ImageVector -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp - -@Composable -fun NuvioEmptyState( - title: String, - message: String? = null, - modifier: Modifier = Modifier, - icon: ImageVector = Icons.Rounded.SearchOff, - iconPainter: Painter? = null, - actionLabel: String? = null, - onActionClick: (() -> Unit)? = null, -) { - Column( - modifier = modifier - .fillMaxWidth() - .padding(32.dp), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(8.dp, Alignment.CenterVertically), - ) { - if (iconPainter != null) { - Icon( - painter = iconPainter, - contentDescription = null, - modifier = Modifier.size(48.dp), - tint = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f), - ) - } else { - Icon( - imageVector = icon, - contentDescription = null, - modifier = Modifier.size(48.dp), - tint = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f), - ) - } - Spacer(modifier = Modifier.height(8.dp)) - Text( - text = title, - style = MaterialTheme.typography.bodyLarge.copy( - fontSize = 16.sp, - fontWeight = FontWeight.SemiBold, - ), - color = MaterialTheme.colorScheme.onSurfaceVariant, - textAlign = TextAlign.Center, - ) - if (!message.isNullOrBlank()) { - Text( - text = message, - style = MaterialTheme.typography.bodySmall.copy(fontSize = 14.sp), - color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f), - textAlign = TextAlign.Center, - ) - } - if (actionLabel != null && onActionClick != null) { - Spacer(modifier = Modifier.height(8.dp)) - NuvioPrimaryButton( - text = actionLabel, - onClick = onActionClick, - ) - } - } -} diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/addons/AddonsScreen.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/addons/AddonsScreen.kt index 57ddd621f..2d7db4a85 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/addons/AddonsScreen.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/addons/AddonsScreen.kt @@ -9,7 +9,6 @@ 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.heightIn import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width @@ -48,7 +47,6 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.nuvio.app.core.build.AppFeaturePolicy import coil3.compose.AsyncImage import com.nuvio.app.core.ui.NuvioIconActionButton -import com.nuvio.app.core.ui.NuvioEmptyState import com.nuvio.app.core.ui.NuvioInfoBadge import com.nuvio.app.core.ui.NuvioInputField import com.nuvio.app.core.ui.NuvioPrimaryButton @@ -96,7 +94,7 @@ internal fun AddonsSettingsPageContent( var formMessage by rememberSaveable { mutableStateOf(null) } var installModalState by remember { mutableStateOf(null) } val enterAddonUrlMessage = stringResource(Res.string.addons_error_enter_url) - val useStoreNarrative = AppFeaturePolicy.storeNarrativeEnabled + val usePersonalMediaCopy = AppFeaturePolicy.personalMediaAddonCopyEnabled val overview = remember(uiState.addons) { uiState.addons.toOverview() } @@ -111,7 +109,7 @@ internal fun AddonsSettingsPageContent( AddAddonCard( addonUrl = addonUrl, formMessage = formMessage, - useStoreNarrative = useStoreNarrative, + usePersonalMediaCopy = usePersonalMediaCopy, onAddonUrlChange = { addonUrl = it formMessage = null @@ -143,7 +141,7 @@ internal fun AddonsSettingsPageContent( SectionHeader(stringResource(Res.string.addons_section_installed)) if (uiState.addons.isEmpty()) { - EmptyStateCard(useStoreNarrative = useStoreNarrative) + EmptyStateCard(usePersonalMediaCopy = usePersonalMediaCopy) } else { val lastIndex = uiState.addons.lastIndex uiState.addons.forEachIndexed { index, addon -> @@ -288,7 +286,7 @@ private fun VerticalSeparator() { private fun AddAddonCard( addonUrl: String, formMessage: String?, - useStoreNarrative: Boolean, + usePersonalMediaCopy: Boolean, onAddonUrlChange: (String) -> Unit, onAddClick: () -> Unit, ) { @@ -296,15 +294,27 @@ private fun AddAddonCard( NuvioInputField( value = addonUrl, onValueChange = onAddonUrlChange, - placeholder = stringResource(Res.string.addons_input_placeholder), + placeholder = stringResource( + if (usePersonalMediaCopy) { + Res.string.addons_appstore_input_placeholder + } else { + Res.string.addons_input_placeholder + }, + ), ) Spacer(modifier = Modifier.height(18.dp)) NuvioPrimaryButton( - text = stringResource(Res.string.addons_install_button), + text = stringResource( + if (usePersonalMediaCopy) { + Res.string.addons_appstore_install_button + } else { + Res.string.addons_install_button + }, + ), enabled = addonUrl.isNotBlank(), onClick = onAddClick, ) - if (useStoreNarrative) { + if (usePersonalMediaCopy) { Spacer(modifier = Modifier.height(14.dp)) Text( text = stringResource(Res.string.addons_appstore_add_description), @@ -345,26 +355,33 @@ private sealed interface AddonInstallModalState { @Composable private fun EmptyStateCard( - useStoreNarrative: Boolean, + usePersonalMediaCopy: Boolean, ) { - NuvioEmptyState( - modifier = Modifier.heightIn(min = 220.dp), - icon = Icons.Rounded.Extension, - title = stringResource( - if (useStoreNarrative) { - Res.string.addons_appstore_empty_title - } else { - Res.string.addons_empty_title - }, - ), - message = stringResource( - if (useStoreNarrative) { - Res.string.addons_appstore_empty_subtitle - } else { - Res.string.addons_empty_subtitle - }, - ), - ) + NuvioSurfaceCard { + Text( + text = stringResource( + if (usePersonalMediaCopy) { + Res.string.addons_appstore_empty_title + } else { + Res.string.addons_empty_title + }, + ), + style = MaterialTheme.typography.titleLarge, + color = MaterialTheme.colorScheme.onSurface, + ) + Spacer(modifier = Modifier.height(8.dp)) + Text( + text = stringResource( + if (usePersonalMediaCopy) { + Res.string.addons_appstore_empty_subtitle + } else { + Res.string.addons_empty_subtitle + }, + ), + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } } @Composable diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/catalog/CatalogScreen.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/catalog/CatalogScreen.kt index 362fe6d84..c11312d5e 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/catalog/CatalogScreen.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/catalog/CatalogScreen.kt @@ -23,7 +23,6 @@ import androidx.compose.foundation.lazy.grid.items import androidx.compose.foundation.lazy.grid.rememberLazyGridState import androidx.compose.foundation.shape.RoundedCornerShape import com.nuvio.app.core.ui.NuvioLoadingIndicator -import com.nuvio.app.core.ui.NuvioEmptyState import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -163,7 +162,6 @@ fun CatalogScreen( .background(MaterialTheme.colorScheme.background), ) { val columns = remember(maxWidth) { catalogGridColumnsForWidth(maxWidth) } - val viewportHeight = maxHeight Box(modifier = Modifier.fillMaxSize()) { LazyVerticalGrid( @@ -186,9 +184,6 @@ fun CatalogScreen( } else if (uiState.items.isEmpty()) { item(span = { GridItemSpan(maxLineSpan) }) { CatalogEmptyState( - modifier = Modifier.height( - (viewportHeight - 120.dp).coerceAtLeast(280.dp), - ), errorMessage = uiState.errorMessage, networkCondition = networkStatusUiState.condition, onRetry = { @@ -366,7 +361,6 @@ private fun CatalogEmptyState( errorMessage: String?, networkCondition: NetworkCondition, onRetry: (() -> Unit)? = null, - modifier: Modifier = Modifier, ) { if (networkCondition == NetworkCondition.NoInternet || networkCondition == NetworkCondition.ServersUnreachable) { NuvioNetworkOfflineCard( @@ -376,11 +370,24 @@ private fun CatalogEmptyState( return } - NuvioEmptyState( - modifier = modifier, - title = stringResource(Res.string.catalog_empty_title), - message = errorMessage ?: stringResource(Res.string.catalog_empty_message), - ) + Column( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 48.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(10.dp), + ) { + Text( + text = stringResource(Res.string.catalog_empty_title), + style = MaterialTheme.typography.titleLarge, + color = MaterialTheme.colorScheme.onBackground, + ) + Text( + text = errorMessage ?: stringResource(Res.string.catalog_empty_message), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } } @Composable diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/downloads/DownloadsScreen.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/downloads/DownloadsScreen.kt index d96fae0bf..ef6a1eb2c 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/downloads/DownloadsScreen.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/downloads/DownloadsScreen.kt @@ -2,6 +2,7 @@ package com.nuvio.app.features.downloads import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer @@ -13,7 +14,7 @@ import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.foundation.lazy.items import androidx.compose.material.icons.Icons import androidx.compose.material.icons.rounded.Delete -import androidx.compose.material.icons.rounded.Download +import androidx.compose.material.icons.rounded.Folder import androidx.compose.material.icons.rounded.Pause import androidx.compose.material.icons.rounded.PlayArrow import androidx.compose.material.icons.rounded.Refresh @@ -37,8 +38,8 @@ import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.nuvio.app.core.i18n.localizedByteUnit import com.nuvio.app.core.ui.NuvioScreen -import com.nuvio.app.core.ui.NuvioEmptyState import com.nuvio.app.core.ui.NuvioScreenHeader +import com.nuvio.app.core.ui.NuvioToastController import nuvio.composeapp.generated.resources.* import org.jetbrains.compose.resources.stringResource @@ -56,6 +57,8 @@ fun DownloadsScreen( }.collectAsStateWithLifecycle() var selectedShowId by rememberSaveable(initialShowId) { mutableStateOf(initialShowId) } + val openDownloadsDirectoryFailedText = stringResource(Res.string.downloads_open_directory_failed) + val completedEpisodes = remember(uiState.items) { uiState.completedItems .filter { it.isEpisode } @@ -83,6 +86,20 @@ fun DownloadsScreen( onBack() } }, + actions = { + IconButton( + onClick = { + if (!DownloadsPlatformDownloader.openDownloadsDirectory()) { + NuvioToastController.show(openDownloadsDirectoryFailedText) + } + }, + ) { + Icon( + imageVector = Icons.Rounded.Folder, + contentDescription = stringResource(Res.string.downloads_open_directory), + ) + } + }, ) } @@ -211,11 +228,18 @@ private fun LazyListScope.downloadsRootContent( if (uiState.items.isEmpty()) { item { - NuvioEmptyState( - modifier = Modifier.fillParentMaxHeight(), - icon = Icons.Rounded.Download, - title = stringResource(Res.string.downloads_empty_title), - ) + Box( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 20.dp, vertical = 40.dp), + contentAlignment = Alignment.Center, + ) { + Text( + text = stringResource(Res.string.downloads_empty_title), + style = MaterialTheme.typography.titleMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } } } } @@ -240,11 +264,18 @@ private fun LazyListScope.downloadsShowContent( if (seasons.isEmpty()) { item { - NuvioEmptyState( - modifier = Modifier.fillParentMaxHeight(), - icon = Icons.Rounded.Download, - title = stringResource(Res.string.downloads_empty_episodes), - ) + Box( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 20.dp, vertical = 40.dp), + contentAlignment = Alignment.Center, + ) { + Text( + text = stringResource(Res.string.downloads_empty_episodes), + style = MaterialTheme.typography.titleMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } } return } diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/home/HomeScreen.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/home/HomeScreen.kt index d2642216b..0cb700758 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/home/HomeScreen.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/home/HomeScreen.kt @@ -15,7 +15,6 @@ import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.nuvio.app.core.auth.AuthRepository import com.nuvio.app.core.auth.AuthState -import com.nuvio.app.core.build.AppFeaturePolicy import com.nuvio.app.core.network.NetworkCondition import com.nuvio.app.core.network.NetworkStatusRepository import com.nuvio.app.core.ui.LocalNuvioBottomNavigationOverlayPadding @@ -37,6 +36,7 @@ import com.nuvio.app.features.details.seriesPrimaryAction import com.nuvio.app.features.home.components.HomeCatalogRowSection import com.nuvio.app.features.home.components.HomeContinueWatchingSection import com.nuvio.app.features.home.components.HomeEmptyStateCard +import com.nuvio.app.features.home.components.HomeHeroReservedSpace import com.nuvio.app.features.home.components.HomeHeroSection import com.nuvio.app.features.home.components.HomeSkeletonHero import com.nuvio.app.features.home.components.HomeSkeletonRow @@ -107,6 +107,7 @@ fun HomeScreen( onFirstCatalogRendered: (() -> Unit)? = null, ) { LaunchedEffect(Unit) { + AddonRepository.initialize() CollectionRepository.initialize() ContinueWatchingPreferencesRepository.ensureLoaded() WatchedRepository.ensureLoaded() @@ -117,10 +118,7 @@ fun HomeScreen( } } - val addonsUiState by remember { - AddonRepository.initialize() - AddonRepository.uiState - }.collectAsStateWithLifecycle() + val addonsUiState by AddonRepository.uiState.collectAsStateWithLifecycle() val homeUiState by HomeRepository.uiState.collectAsStateWithLifecycle() val homeSettingsUiState by remember { HomeCatalogSettingsRepository.snapshot() @@ -442,6 +440,7 @@ fun HomeScreen( } LaunchedEffect(catalogRefreshKey) { + if (catalogRefreshKey.isEmpty()) return@LaunchedEffect HomeCatalogSettingsRepository.syncCatalogs(enabledAddons) HomeRepository.refresh(enabledAddons) } @@ -656,15 +655,11 @@ fun HomeScreen( } val hasActiveAddons = enabledAddons.any { it.manifest != null } - val hasCurrentHeroSource = enabledAddons.any { it.manifest != null || it.isRefreshing } || - collections.any { it.folders.isNotEmpty() } + val showHeroSlot = homeSettingsUiState.heroEnabled val isResolvingHeroSources = enabledAddons.any { it.isRefreshing } || homeUiState.isLoading - val showHeroSkeleton = homeSettingsUiState.heroEnabled && - enabledAddons.isNotEmpty() && + val showHeroSkeleton = showHeroSlot && homeUiState.heroItems.isEmpty() && isResolvingHeroSources - val showHeroSlot = homeSettingsUiState.heroEnabled && hasCurrentHeroSource && - (homeUiState.heroItems.isNotEmpty() || showHeroSkeleton) var firstCatalogReported by remember { mutableStateOf(false) } LaunchedEffect(homeUiState.sections.firstOrNull()?.key, onFirstCatalogRendered) { @@ -754,14 +749,14 @@ fun HomeScreen( ) { if (showHeroSlot) { item { - if (showHeroSkeleton) { - HomeSkeletonHero( + when { + showHeroSkeleton -> HomeSkeletonHero( modifier = Modifier, viewportHeight = maxHeight, mobileBelowSectionHeightHint = mobileHeroBelowSectionHeightHint, ) - } else { - HomeHeroSection( + + homeUiState.heroItems.isNotEmpty() -> HomeHeroSection( items = homeUiState.heroItems, modifier = Modifier, viewportHeight = maxHeight, @@ -769,6 +764,12 @@ fun HomeScreen( listState = homeListState, onItemClick = onPosterClick, ) + + else -> HomeHeroReservedSpace( + modifier = Modifier, + viewportHeight = maxHeight, + mobileBelowSectionHeightHint = mobileHeroBelowSectionHeightHint, + ) } } } @@ -791,31 +792,10 @@ fun HomeScreen( } } item { - val emptyStateModifier = if ( - continueWatchingPreferences.isVisible && continueWatchingItems.isNotEmpty() - ) { - Modifier.padding(horizontal = 16.dp) - } else { - Modifier - .fillParentMaxHeight() - .padding(horizontal = 16.dp) - } HomeEmptyStateCard( - modifier = emptyStateModifier, - title = stringResource( - if (AppFeaturePolicy.storeNarrativeEnabled) { - Res.string.addons_empty_title - } else { - Res.string.compose_search_empty_no_active_addons_title - }, - ), - message = stringResource( - if (AppFeaturePolicy.storeNarrativeEnabled) { - Res.string.addons_appstore_empty_subtitle - } else { - Res.string.home_empty_no_active_addons_message - }, - ), + modifier = Modifier.padding(horizontal = 16.dp), + title = stringResource(Res.string.compose_search_empty_no_active_addons_title), + message = stringResource(Res.string.home_empty_no_active_addons_message), ) } } @@ -859,22 +839,10 @@ fun HomeScreen( ) } else { HomeEmptyStateCard( - modifier = Modifier - .fillParentMaxHeight() - .padding(horizontal = 16.dp), - title = stringResource( - if (AppFeaturePolicy.storeNarrativeEnabled) { - Res.string.store_empty_unavailable_title - } else { - Res.string.home_empty_no_rows_title - }, - ), - message = if (AppFeaturePolicy.storeNarrativeEnabled) { - stringResource(Res.string.store_empty_unavailable_message) - } else { - homeUiState.errorMessage - ?: stringResource(Res.string.home_empty_no_rows_message) - }, + modifier = Modifier.padding(horizontal = 16.dp), + title = stringResource(Res.string.home_empty_no_rows_title), + message = homeUiState.errorMessage + ?: stringResource(Res.string.home_empty_no_rows_message), ) } } diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/home/components/HomeStateCards.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/home/components/HomeStateCards.kt index 262a9eefc..ad8db1533 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/home/components/HomeStateCards.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/home/components/HomeStateCards.kt @@ -1,25 +1,41 @@ package com.nuvio.app.features.home.components +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.painter.Painter -import com.nuvio.app.core.ui.NuvioEmptyState +import androidx.compose.ui.unit.dp +import com.nuvio.app.core.ui.NuvioPrimaryButton +import com.nuvio.app.core.ui.NuvioSurfaceCard @Composable fun HomeEmptyStateCard( title: String, message: String, modifier: Modifier = Modifier, - iconPainter: Painter? = null, actionLabel: String? = null, onActionClick: (() -> Unit)? = null, ) { - NuvioEmptyState( - modifier = modifier, - title = title, - message = message, - iconPainter = iconPainter, - actionLabel = actionLabel, - onActionClick = onActionClick, - ) + NuvioSurfaceCard(modifier = modifier) { + Text( + text = title, + style = MaterialTheme.typography.titleLarge, + color = MaterialTheme.colorScheme.onSurface, + ) + Spacer(modifier = Modifier.height(8.dp)) + Text( + text = message, + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + if (actionLabel != null && onActionClick != null) { + Spacer(modifier = Modifier.height(16.dp)) + NuvioPrimaryButton( + text = actionLabel, + onClick = onActionClick, + ) + } + } } diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/library/LibraryScreen.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/library/LibraryScreen.kt index 4f06bff85..9e29c5776 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/library/LibraryScreen.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/library/LibraryScreen.kt @@ -84,7 +84,6 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.launch import nuvio.composeapp.generated.resources.* -import org.jetbrains.compose.resources.painterResource import org.jetbrains.compose.resources.stringResource @Composable @@ -269,10 +268,7 @@ fun LibraryScreen( ) } else { HomeEmptyStateCard( - modifier = Modifier - .fillParentMaxHeight(0.55f) - .padding(horizontal = 16.dp), - iconPainter = painterResource(Res.drawable.sidebar_library), + modifier = Modifier.padding(horizontal = 16.dp), title = if (isTraktSource) { stringResource(Res.string.library_trakt_load_failed) } else { @@ -296,10 +292,7 @@ fun LibraryScreen( ) } else { HomeEmptyStateCard( - modifier = Modifier - .fillParentMaxHeight(0.55f) - .padding(horizontal = 16.dp), - iconPainter = painterResource(Res.drawable.sidebar_library), + modifier = Modifier.padding(horizontal = 16.dp), title = if (isTraktSource) { stringResource(Res.string.library_trakt_empty_title) } else { @@ -352,10 +345,7 @@ private fun LazyListScope.cloudLibraryContent( !uiState.isEnabled -> { item { HomeEmptyStateCard( - modifier = Modifier - .fillParentMaxHeight(0.55f) - .padding(horizontal = 16.dp), - iconPainter = painterResource(Res.drawable.sidebar_library), + modifier = Modifier.padding(horizontal = 16.dp), title = stringResource(Res.string.cloud_library_disabled_title), message = stringResource(Res.string.cloud_library_disabled_message), actionLabel = stringResource(Res.string.cloud_library_disabled_action), @@ -367,10 +357,7 @@ private fun LazyListScope.cloudLibraryContent( !uiState.hasConnectedProvider -> { item { HomeEmptyStateCard( - modifier = Modifier - .fillParentMaxHeight(0.55f) - .padding(horizontal = 16.dp), - iconPainter = painterResource(Res.drawable.sidebar_library), + modifier = Modifier.padding(horizontal = 16.dp), title = stringResource(Res.string.cloud_library_connect_title), message = stringResource(Res.string.cloud_library_connect_message), actionLabel = stringResource(Res.string.cloud_library_connect_action), @@ -420,7 +407,6 @@ private fun LazyListScope.cloudLibraryContent( item(key = "cloud-error-${providerState.providerId}") { HomeEmptyStateCard( modifier = Modifier.padding(horizontal = 16.dp), - iconPainter = painterResource(Res.drawable.sidebar_library), title = stringResource(Res.string.cloud_library_load_failed, providerState.providerName), message = providerState.errorMessage.orEmpty(), actionLabel = stringResource(Res.string.action_retry), @@ -434,10 +420,7 @@ private fun LazyListScope.cloudLibraryContent( } else if (filteredItems.isEmpty()) { item { HomeEmptyStateCard( - modifier = Modifier - .fillParentMaxHeight(0.5f) - .padding(horizontal = 16.dp), - iconPainter = painterResource(Res.drawable.sidebar_library), + modifier = Modifier.padding(horizontal = 16.dp), title = stringResource(Res.string.cloud_library_empty_title), message = stringResource(Res.string.cloud_library_empty_message), actionLabel = stringResource(Res.string.action_retry), diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/search/SearchDiscoverContent.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/search/SearchDiscoverContent.kt index d3221b88c..ed58b7f28 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/search/SearchDiscoverContent.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/search/SearchDiscoverContent.kt @@ -31,7 +31,6 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import coil3.compose.AsyncImage -import com.nuvio.app.core.build.AppFeaturePolicy import com.nuvio.app.core.network.NetworkCondition import com.nuvio.app.core.format.formatReleaseDateForDisplay import com.nuvio.app.core.ui.NuvioDropdownChip @@ -95,9 +94,7 @@ internal fun LazyListScope.discoverContent( items(2) { DiscoverSkeletonRow( columns = columns, - modifier = Modifier - .fillParentMaxHeight(0.72f) - .padding(horizontal = 16.dp), + modifier = Modifier.padding(horizontal = 16.dp), ) } } @@ -356,37 +353,13 @@ private fun DiscoverEmptyStateCard( when (reason) { DiscoverEmptyStateReason.NoActiveAddons -> { - title = stringResource( - if (AppFeaturePolicy.storeNarrativeEnabled) { - Res.string.addons_empty_title - } else { - Res.string.compose_search_empty_no_active_addons_title - }, - ) - message = stringResource( - if (AppFeaturePolicy.storeNarrativeEnabled) { - Res.string.addons_appstore_empty_subtitle - } else { - Res.string.discover_empty_no_active_addons_message - }, - ) + title = stringResource(Res.string.compose_search_empty_no_active_addons_title) + message = stringResource(Res.string.discover_empty_no_active_addons_message) } DiscoverEmptyStateReason.NoDiscoverCatalogs -> { - title = stringResource( - if (AppFeaturePolicy.storeNarrativeEnabled) { - Res.string.store_empty_unavailable_title - } else { - Res.string.discover_empty_no_catalogs_title - }, - ) - message = stringResource( - if (AppFeaturePolicy.storeNarrativeEnabled) { - Res.string.store_empty_unavailable_message - } else { - Res.string.discover_empty_no_catalogs_message - }, - ) + title = stringResource(Res.string.discover_empty_no_catalogs_title) + message = stringResource(Res.string.discover_empty_no_catalogs_message) } DiscoverEmptyStateReason.RequestFailed -> { diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/search/SearchScreen.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/search/SearchScreen.kt index 0448e3689..4520befe5 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/search/SearchScreen.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/search/SearchScreen.kt @@ -40,7 +40,6 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.nuvio.app.core.build.AppFeaturePolicy import com.nuvio.app.core.network.NetworkCondition import com.nuvio.app.core.network.NetworkStatusRepository import com.nuvio.app.core.ui.NuvioInputField @@ -65,8 +64,6 @@ import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.map import nuvio.composeapp.generated.resources.Res -import nuvio.composeapp.generated.resources.addons_appstore_empty_subtitle -import nuvio.composeapp.generated.resources.addons_empty_title import nuvio.composeapp.generated.resources.compose_nav_search import nuvio.composeapp.generated.resources.compose_search_clear import nuvio.composeapp.generated.resources.compose_search_discover_title @@ -81,8 +78,6 @@ import nuvio.composeapp.generated.resources.compose_search_empty_no_search_catal import nuvio.composeapp.generated.resources.compose_search_placeholder import nuvio.composeapp.generated.resources.compose_search_recent_searches import nuvio.composeapp.generated.resources.compose_search_remove_recent_search -import nuvio.composeapp.generated.resources.store_empty_unavailable_message -import nuvio.composeapp.generated.resources.store_empty_unavailable_title import org.jetbrains.compose.resources.stringResource @Composable @@ -322,9 +317,7 @@ fun SearchScreen( isWaitingForSearch -> { items(2) { HomeSkeletonRow( - modifier = Modifier - .fillParentMaxHeight() - .padding(horizontal = homeSectionPadding), + modifier = Modifier.padding(horizontal = homeSectionPadding), showHeaderAccent = !homeCatalogSettingsUiState.hideCatalogUnderline, ) } @@ -420,37 +413,13 @@ private fun SearchEmptyStateCard( when (reason) { SearchEmptyStateReason.NoActiveAddons -> { - title = stringResource( - if (AppFeaturePolicy.storeNarrativeEnabled) { - Res.string.addons_empty_title - } else { - Res.string.compose_search_empty_no_active_addons_title - }, - ) - message = stringResource( - if (AppFeaturePolicy.storeNarrativeEnabled) { - Res.string.addons_appstore_empty_subtitle - } else { - Res.string.compose_search_empty_no_active_addons_message - }, - ) + title = stringResource(Res.string.compose_search_empty_no_active_addons_title) + message = stringResource(Res.string.compose_search_empty_no_active_addons_message) } SearchEmptyStateReason.NoSearchCatalogs -> { - title = stringResource( - if (AppFeaturePolicy.storeNarrativeEnabled) { - Res.string.store_empty_unavailable_title - } else { - Res.string.compose_search_empty_no_search_catalogs_title - }, - ) - message = stringResource( - if (AppFeaturePolicy.storeNarrativeEnabled) { - Res.string.store_empty_unavailable_message - } else { - Res.string.compose_search_empty_no_search_catalogs_message - }, - ) + title = stringResource(Res.string.compose_search_empty_no_search_catalogs_title) + message = stringResource(Res.string.compose_search_empty_no_search_catalogs_message) } SearchEmptyStateReason.RequestFailed -> { diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/ContentDiscoverySettingsPage.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/ContentDiscoverySettingsPage.kt index 51188e97d..e4d923875 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/ContentDiscoverySettingsPage.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/ContentDiscoverySettingsPage.kt @@ -19,20 +19,14 @@ internal fun LazyListScope.contentDiscoveryContent( ) { item { SettingsSection( - title = stringResource( - if (AppFeaturePolicy.storeNarrativeEnabled) { - Res.string.compose_settings_page_addons - } else { - Res.string.settings_content_discovery_section_sources - }, - ), + title = stringResource(Res.string.settings_content_discovery_section_sources), isTablet = isTablet, ) { SettingsGroup(isTablet = isTablet) { SettingsNavigationRow( title = stringResource(Res.string.compose_settings_page_addons), description = stringResource( - if (AppFeaturePolicy.storeNarrativeEnabled) { + if (AppFeaturePolicy.personalMediaAddonCopyEnabled) { Res.string.settings_content_discovery_addons_description_appstore } else { Res.string.settings_content_discovery_addons_description diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/HomescreenSettingsPage.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/HomescreenSettingsPage.kt index 43ce15194..254d49e18 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/HomescreenSettingsPage.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/HomescreenSettingsPage.kt @@ -31,7 +31,6 @@ import androidx.compose.ui.hapticfeedback.HapticFeedbackType import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp -import com.nuvio.app.core.build.AppFeaturePolicy import com.nuvio.app.core.ui.NuvioActionLabel import com.nuvio.app.core.ui.NuvioToastController import com.nuvio.app.features.home.HomeCatalogSettingsItem @@ -59,8 +58,6 @@ import nuvio.composeapp.generated.resources.settings_homescreen_show_hero import nuvio.composeapp.generated.resources.settings_homescreen_show_hero_description import nuvio.composeapp.generated.resources.settings_homescreen_summary import nuvio.composeapp.generated.resources.settings_homescreen_summary_hint -import nuvio.composeapp.generated.resources.store_empty_unavailable_message -import nuvio.composeapp.generated.resources.store_empty_unavailable_title import org.jetbrains.compose.resources.stringResource import sh.calvin.reorderable.ReorderableCollectionItemScope import sh.calvin.reorderable.ReorderableItem @@ -136,23 +133,9 @@ internal fun LazyListScope.homescreenSettingsContent( item { if (items.isEmpty()) { HomeEmptyStateCard( - modifier = Modifier - .fillMaxWidth() - .heightIn(min = 280.dp), - title = stringResource( - if (AppFeaturePolicy.storeNarrativeEnabled) { - Res.string.store_empty_unavailable_title - } else { - Res.string.settings_homescreen_empty_title - }, - ), - message = stringResource( - if (AppFeaturePolicy.storeNarrativeEnabled) { - Res.string.store_empty_unavailable_message - } else { - Res.string.settings_homescreen_empty_message - }, - ), + modifier = Modifier.fillMaxWidth(), + title = stringResource(Res.string.settings_homescreen_empty_title), + message = stringResource(Res.string.settings_homescreen_empty_message), ) } else { val catalogCount = items.count { !it.isCollection } diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/SettingsRootPage.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/SettingsRootPage.kt index fb460046d..1ed8b5d86 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/SettingsRootPage.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/SettingsRootPage.kt @@ -20,7 +20,6 @@ import androidx.compose.material3.Text import androidx.compose.ui.Modifier import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp -import com.nuvio.app.core.build.AppFeaturePolicy import com.nuvio.app.core.build.AppVersionConfig import nuvio.composeapp.generated.resources.Res import nuvio.composeapp.generated.resources.compose_about_made_with @@ -38,9 +37,7 @@ import nuvio.composeapp.generated.resources.compose_settings_root_appearance_des import nuvio.composeapp.generated.resources.compose_settings_root_check_updates_description import nuvio.composeapp.generated.resources.compose_settings_root_check_updates_title import nuvio.composeapp.generated.resources.compose_settings_root_content_discovery_description -import nuvio.composeapp.generated.resources.compose_settings_root_content_discovery_description_appstore import nuvio.composeapp.generated.resources.compose_settings_root_downloads_description -import nuvio.composeapp.generated.resources.compose_settings_root_downloads_description_appstore import nuvio.composeapp.generated.resources.compose_settings_root_downloads_title import nuvio.composeapp.generated.resources.compose_settings_root_general_section import nuvio.composeapp.generated.resources.compose_settings_root_integrations_description @@ -133,13 +130,7 @@ internal fun LazyListScope.settingsRootContent( SettingsGroupDivider(isTablet = isTablet) SettingsNavigationRow( title = stringResource(Res.string.compose_settings_page_content_discovery), - description = stringResource( - if (AppFeaturePolicy.storeNarrativeEnabled) { - Res.string.compose_settings_root_content_discovery_description_appstore - } else { - Res.string.compose_settings_root_content_discovery_description - }, - ), + description = stringResource(Res.string.compose_settings_root_content_discovery_description), icon = Icons.Rounded.Extension, isTablet = isTablet, onClick = onContentDiscoveryClick, @@ -147,13 +138,7 @@ internal fun LazyListScope.settingsRootContent( SettingsGroupDivider(isTablet = isTablet) SettingsNavigationRow( title = stringResource(Res.string.compose_settings_root_downloads_title), - description = stringResource( - if (AppFeaturePolicy.storeNarrativeEnabled) { - Res.string.compose_settings_root_downloads_description_appstore - } else { - Res.string.compose_settings_root_downloads_description - }, - ), + description = stringResource(Res.string.compose_settings_root_downloads_description), icon = Icons.Rounded.CloudDownload, isTablet = isTablet, onClick = onDownloadsClick, diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/SettingsScreen.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/SettingsScreen.kt index e21a8032f..6f42f03ef 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/SettingsScreen.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/SettingsScreen.kt @@ -567,7 +567,7 @@ private fun MobileSettingsScreen( pluginsEnabled = AppFeaturePolicy.pluginsEnabled, supportersContributorsPageEnabled = AppFeaturePolicy.supportersContributorsPageEnabled, accountDeletionEnabled = AppFeaturePolicy.accountDeletionEnabled, - storeNarrativeEnabled = AppFeaturePolicy.storeNarrativeEnabled, + personalMediaAddonCopyEnabled = AppFeaturePolicy.personalMediaAddonCopyEnabled, liquidGlassNativeTabBarSupported = liquidGlassNativeTabBarSupported, switchProfileAvailable = onSwitchProfile != null, checkForUpdatesAvailable = onCheckForUpdatesClick != null, @@ -955,7 +955,7 @@ private fun TabletSettingsScreen( pluginsEnabled = AppFeaturePolicy.pluginsEnabled, supportersContributorsPageEnabled = AppFeaturePolicy.supportersContributorsPageEnabled, accountDeletionEnabled = AppFeaturePolicy.accountDeletionEnabled, - storeNarrativeEnabled = AppFeaturePolicy.storeNarrativeEnabled, + personalMediaAddonCopyEnabled = AppFeaturePolicy.personalMediaAddonCopyEnabled, liquidGlassNativeTabBarSupported = liquidGlassNativeTabBarSupported, switchProfileAvailable = onSwitchProfile != null, checkForUpdatesAvailable = onCheckForUpdatesClick != null, diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/SettingsSearch.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/SettingsSearch.kt index 47206f871..419d5567f 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/SettingsSearch.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/SettingsSearch.kt @@ -8,7 +8,6 @@ import androidx.compose.animation.fadeIn import androidx.compose.animation.slideInVertically import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.material.icons.Icons @@ -44,7 +43,6 @@ import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import com.nuvio.app.core.ui.NuvioTokens -import com.nuvio.app.core.ui.NuvioEmptyState import com.nuvio.app.core.ui.nuvio import com.nuvio.app.isIos import nuvio.composeapp.generated.resources.* @@ -83,7 +81,7 @@ internal fun settingsSearchEntries( pluginsEnabled: Boolean, supportersContributorsPageEnabled: Boolean, accountDeletionEnabled: Boolean, - storeNarrativeEnabled: Boolean, + personalMediaAddonCopyEnabled: Boolean, liquidGlassNativeTabBarSupported: Boolean, switchProfileAvailable: Boolean, checkForUpdatesAvailable: Boolean, @@ -227,25 +225,13 @@ internal fun settingsSearchEntries( page = SettingsPage.ContentDiscovery, key = "content-discovery", title = contentDiscoveryPage, - description = stringResource( - if (storeNarrativeEnabled) { - Res.string.compose_settings_root_content_discovery_description_appstore - } else { - Res.string.compose_settings_root_content_discovery_description - }, - ), + description = stringResource(Res.string.compose_settings_root_content_discovery_description), icon = Icons.Rounded.Extension, ) add( key = "downloads", title = downloadsPage, - description = stringResource( - if (storeNarrativeEnabled) { - Res.string.compose_settings_root_downloads_description_appstore - } else { - Res.string.compose_settings_root_downloads_description - }, - ), + description = stringResource(Res.string.compose_settings_root_downloads_description), category = generalCategory, icon = Icons.Rounded.CloudDownload, target = SettingsSearchTarget.Downloads, @@ -462,7 +448,7 @@ internal fun settingsSearchEntries( key = "addons", title = addonsPage, description = stringResource( - if (storeNarrativeEnabled) { + if (personalMediaAddonCopyEnabled) { Res.string.settings_content_discovery_addons_description_appstore } else { Res.string.settings_content_discovery_addons_description @@ -1120,14 +1106,25 @@ private fun SettingsSearchField( @Composable private fun SettingsSearchEmptyState(isTablet: Boolean) { + val tokens = MaterialTheme.nuvio SettingsSection( title = stringResource(Res.string.settings_search_results_section), isTablet = isTablet, ) { - NuvioEmptyState( - modifier = Modifier.heightIn(min = 280.dp), - title = stringResource(Res.string.settings_search_empty), - ) + SettingsGroup(isTablet = isTablet) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = if (isTablet) 20.dp else 16.dp, vertical = 18.dp), + ) { + Text( + text = stringResource(Res.string.settings_search_empty), + style = MaterialTheme.typography.bodyLarge, + color = tokens.colors.textPrimary, + fontWeight = FontWeight.Medium, + ) + } + } } } diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/streams/StreamsScreen.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/streams/StreamsScreen.kt index e94adf57b..0d3e16772 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/streams/StreamsScreen.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/streams/StreamsScreen.kt @@ -41,6 +41,7 @@ import androidx.compose.material.icons.automirrored.rounded.OpenInNew import androidx.compose.material.icons.rounded.ContentCopy import androidx.compose.material.icons.rounded.Download import androidx.compose.material.icons.rounded.Refresh +import androidx.compose.material.icons.rounded.SearchOff import com.nuvio.app.core.ui.NuvioLoadingIndicator import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme @@ -74,7 +75,6 @@ import com.nuvio.app.core.build.AppFeaturePolicy import com.nuvio.app.core.ui.NuvioBackButton import com.nuvio.app.core.ui.NuvioBottomSheetActionRow import com.nuvio.app.core.ui.NuvioBottomSheetDivider -import com.nuvio.app.core.ui.NuvioEmptyState import com.nuvio.app.core.ui.NuvioModalBottomSheet import com.nuvio.app.core.ui.NuvioToastController import com.nuvio.app.core.ui.dismissNuvioBottomSheet @@ -892,10 +892,7 @@ internal fun StreamList( !hasAnyStreams && !uiState.isAnyLoading -> { item { - EmptyStateBlock( - reason = uiState.emptyStateReason, - modifier = Modifier.fillParentMaxHeight(), - ) + EmptyStateBlock(reason = uiState.emptyStateReason) } } @@ -1252,37 +1249,13 @@ private fun EmptyStateBlock( when (reason) { StreamsEmptyStateReason.NoAddonsInstalled -> { - title = stringResource( - if (AppFeaturePolicy.storeNarrativeEnabled) { - Res.string.addons_empty_title - } else { - Res.string.compose_search_empty_no_active_addons_title - }, - ) - message = stringResource( - if (AppFeaturePolicy.storeNarrativeEnabled) { - Res.string.addons_appstore_empty_subtitle - } else { - Res.string.streams_empty_no_addons_message - }, - ) + title = stringResource(Res.string.compose_search_empty_no_active_addons_title) + message = stringResource(Res.string.streams_empty_no_addons_message) } StreamsEmptyStateReason.NoCompatibleAddons -> { - title = stringResource( - if (AppFeaturePolicy.storeNarrativeEnabled) { - Res.string.store_empty_unavailable_title - } else { - Res.string.streams_empty_no_stream_addon_title - }, - ) - message = stringResource( - if (AppFeaturePolicy.storeNarrativeEnabled) { - Res.string.store_empty_unavailable_message - } else { - Res.string.streams_empty_no_stream_addon_message - }, - ) + title = stringResource(Res.string.streams_empty_no_stream_addon_title) + message = stringResource(Res.string.streams_empty_no_stream_addon_message) } StreamsEmptyStateReason.StreamFetchFailed -> { @@ -1296,11 +1269,35 @@ private fun EmptyStateBlock( } } - NuvioEmptyState( - modifier = modifier, - title = title, - message = message, - ) + Column( + modifier = modifier + .fillMaxWidth() + .padding(32.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(8.dp), + ) { + Icon( + imageVector = Icons.Rounded.SearchOff, + contentDescription = null, + modifier = Modifier.size(48.dp), + tint = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f), + ) + Spacer(modifier = Modifier.height(8.dp)) + Text( + text = title, + style = MaterialTheme.typography.bodyLarge.copy( + fontSize = 16.sp, + fontWeight = FontWeight.SemiBold, + ), + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + Text( + text = message, + style = MaterialTheme.typography.bodySmall.copy(fontSize = 14.sp), + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f), + textAlign = TextAlign.Center, + ) + } } @Composable diff --git a/composeApp/src/desktopMain/kotlin/com/nuvio/app/core/build/AppFeaturePolicy.desktop.kt b/composeApp/src/desktopMain/kotlin/com/nuvio/app/core/build/AppFeaturePolicy.desktop.kt index aa8fb74f4..bc70435f0 100644 --- a/composeApp/src/desktopMain/kotlin/com/nuvio/app/core/build/AppFeaturePolicy.desktop.kt +++ b/composeApp/src/desktopMain/kotlin/com/nuvio/app/core/build/AppFeaturePolicy.desktop.kt @@ -4,7 +4,7 @@ actual object AppFeaturePolicy { actual val pluginsEnabled: Boolean = false actual val supportersContributorsPageEnabled: Boolean = true actual val accountDeletionEnabled: Boolean = false - actual val storeNarrativeEnabled: Boolean = false + actual val personalMediaAddonCopyEnabled: Boolean = false actual val p2pEnabled: Boolean = false actual val trailerPlaybackMode: TrailerPlaybackMode = TrailerPlaybackMode.EXTERNAL actual val heroTrailerPlaybackSupported: Boolean = false diff --git a/composeApp/src/iosAppStore/kotlin/com/nuvio/app/core/build/AppFeaturePolicy.ios.kt b/composeApp/src/iosAppStore/kotlin/com/nuvio/app/core/build/AppFeaturePolicy.ios.kt index 4267d3245..6cc679aac 100644 --- a/composeApp/src/iosAppStore/kotlin/com/nuvio/app/core/build/AppFeaturePolicy.ios.kt +++ b/composeApp/src/iosAppStore/kotlin/com/nuvio/app/core/build/AppFeaturePolicy.ios.kt @@ -4,7 +4,7 @@ actual object AppFeaturePolicy { actual val pluginsEnabled: Boolean = false actual val supportersContributorsPageEnabled: Boolean = false actual val accountDeletionEnabled: Boolean = true - actual val storeNarrativeEnabled: Boolean = true + actual val personalMediaAddonCopyEnabled: Boolean = true actual val p2pEnabled: Boolean = false actual val trailerPlaybackMode: TrailerPlaybackMode = TrailerPlaybackMode.EXTERNAL actual val heroTrailerPlaybackSupported: Boolean = false diff --git a/composeApp/src/iosFull/kotlin/com/nuvio/app/core/build/AppFeaturePolicy.ios.kt b/composeApp/src/iosFull/kotlin/com/nuvio/app/core/build/AppFeaturePolicy.ios.kt index b8f0d8371..b86fa9f4b 100644 --- a/composeApp/src/iosFull/kotlin/com/nuvio/app/core/build/AppFeaturePolicy.ios.kt +++ b/composeApp/src/iosFull/kotlin/com/nuvio/app/core/build/AppFeaturePolicy.ios.kt @@ -4,7 +4,7 @@ actual object AppFeaturePolicy { actual val pluginsEnabled: Boolean = true actual val supportersContributorsPageEnabled: Boolean = true actual val accountDeletionEnabled: Boolean = false - actual val storeNarrativeEnabled: Boolean = false + actual val personalMediaAddonCopyEnabled: Boolean = false actual val p2pEnabled: Boolean = false actual val trailerPlaybackMode: TrailerPlaybackMode = TrailerPlaybackMode.IN_APP actual val heroTrailerPlaybackSupported: Boolean = false diff --git a/iosApp/iosApp.xcodeproj/project.pbxproj b/iosApp/iosApp.xcodeproj/project.pbxproj index b0fd8c3fe..2a2c1e595 100644 --- a/iosApp/iosApp.xcodeproj/project.pbxproj +++ b/iosApp/iosApp.xcodeproj/project.pbxproj @@ -246,7 +246,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "if [ \"YES\" = \"$OVERRIDE_KOTLIN_BUILD_IDE_SUPPORTED\" ]; then\n echo \"Skipping Gradle build task invocation due to OVERRIDE_KOTLIN_BUILD_IDE_SUPPORTED environment variable set to \\\"YES\\\"\"\n exit 0\nfi\nexport JAVA_HOME=\"/Applications/Android Studio.app/Contents/jbr/Contents/Home\"\nexport PATH=\"$JAVA_HOME/bin:/opt/homebrew/bin:$PATH\"\nif [ -z \"$GRADLE_OPTS\" ]; then\n export GRADLE_OPTS=\"-Xmx12288M -Dfile.encoding=UTF-8 -XX:MaxMetaspaceSize=2048m\"\nfi\nif [ -z \"$KOTLIN_DAEMON_JVMARGS\" ]; then\n export KOTLIN_DAEMON_JVMARGS=\"-Xmx8192M\"\nfi\ncd \"$SRCROOT/..\"\n./gradlew :composeApp:embedAndSignAppleFrameworkForXcode\n"; + shellScript = "if [ \"YES\" = \"$OVERRIDE_KOTLIN_BUILD_IDE_SUPPORTED\" ]; then\n echo \"Skipping Gradle build task invocation due to OVERRIDE_KOTLIN_BUILD_IDE_SUPPORTED environment variable set to \\\"YES\\\"\"\n exit 0\nfi\nif [ -z \"$JAVA_HOME\" ] && [ -x \"/opt/homebrew/opt/openjdk@17/libexec/openjdk.jdk/Contents/Home/bin/java\" ]; then\n export JAVA_HOME=\"/opt/homebrew/opt/openjdk@17/libexec/openjdk.jdk/Contents/Home\"\nfi\nif [ -n \"$JAVA_HOME\" ]; then\n export PATH=\"$JAVA_HOME/bin:/opt/homebrew/bin:$PATH\"\nelse\n export PATH=\"/opt/homebrew/bin:$PATH\"\nfi\nif [ -z \"$GRADLE_OPTS\" ]; then\n export GRADLE_OPTS=\"-Xmx12288M -Dfile.encoding=UTF-8 -XX:MaxMetaspaceSize=2048m\"\nfi\nif [ -z \"$KOTLIN_DAEMON_JVMARGS\" ]; then\n export KOTLIN_DAEMON_JVMARGS=\"-Xmx8192M\"\nfi\ncd \"$SRCROOT/..\"\n./gradlew :composeApp:embedAndSignAppleFrameworkForXcode\n"; }; /* End PBXShellScriptBuildPhase section */ From 5358c44f1c68c3cca617a434e88765e72e47997f Mon Sep 17 00:00:00 2001 From: tapframe <85391825+tapframe@users.noreply.github.com> Date: Mon, 13 Jul 2026 00:04:12 +0530 Subject: [PATCH 5/6] bump version --- enginefs | 1 + iosApp/Configuration/Version.xcconfig | 4 ++-- stremiotorrernt/public/enginefs | 1 + 3 files changed, 4 insertions(+), 2 deletions(-) create mode 160000 enginefs create mode 160000 stremiotorrernt/public/enginefs diff --git a/enginefs b/enginefs new file mode 160000 index 000000000..9f11922ae --- /dev/null +++ b/enginefs @@ -0,0 +1 @@ +Subproject commit 9f11922ae092e4a7120a7e1bdabb0698df1eecb8 diff --git a/iosApp/Configuration/Version.xcconfig b/iosApp/Configuration/Version.xcconfig index eacc6f18a..a472b4f51 100644 --- a/iosApp/Configuration/Version.xcconfig +++ b/iosApp/Configuration/Version.xcconfig @@ -1,3 +1,3 @@ -CURRENT_PROJECT_VERSION=94 -MARKETING_VERSION=0.2.22 +CURRENT_PROJECT_VERSION=95 +MARKETING_VERSION=0.2.23 diff --git a/stremiotorrernt/public/enginefs b/stremiotorrernt/public/enginefs new file mode 160000 index 000000000..9f11922ae --- /dev/null +++ b/stremiotorrernt/public/enginefs @@ -0,0 +1 @@ +Subproject commit 9f11922ae092e4a7120a7e1bdabb0698df1eecb8 From c0b44c6a33abd83723845e700d272e02826561f0 Mon Sep 17 00:00:00 2001 From: i4mth3d4ng3r <27910294+i4mth3d4ng3r@users.noreply.github.com> Date: Sun, 12 Jul 2026 16:08:58 -0500 Subject: [PATCH 6/6] Update upstream for Parental Guidance api --- .../com/nuvio/app/features/player/ParentalGuideRepository.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/ParentalGuideRepository.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/ParentalGuideRepository.kt index 06fe2fb5a..59e99d3db 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/ParentalGuideRepository.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/ParentalGuideRepository.kt @@ -8,7 +8,7 @@ import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable import kotlinx.serialization.json.Json -private const val PARENTAL_GUIDE_BASE_URL = "https://api.imdbapi.dev" +private const val PARENTAL_GUIDE_BASE_URL = "https://api.tiffara.com" private val imdbIdPattern = Regex("tt\\d+") data class ParentalGuideResult(