mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-07-27 06:52:18 +00:00
feat: add card depth customization
This commit is contained in:
parent
f19d3d1a08
commit
a90ee2fcfb
17 changed files with 924 additions and 9 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -601,6 +601,37 @@
|
|||
<string name="settings_homescreen_visible">Visible</string>
|
||||
<string name="settings_hide_secret">Hide value</string>
|
||||
<string name="settings_playback_subtitle">Player, subtitles, and auto-play</string>
|
||||
<string name="settings_card_depth_title">Card Depth Effect</string>
|
||||
<string name="settings_card_depth_description">Adds a lit top edge and a soft sheen to image cards for a subtle sense of depth.</string>
|
||||
<string name="settings_card_depth_enabled">Enable depth effect</string>
|
||||
<string name="settings_card_depth_edge">Edge glow</string>
|
||||
<string name="settings_card_depth_edge_subtle">Subtle</string>
|
||||
<string name="settings_card_depth_edge_balanced">Balanced</string>
|
||||
<string name="settings_card_depth_edge_bold">Bold</string>
|
||||
<string name="settings_card_depth_sheen">Top sheen</string>
|
||||
<string name="settings_card_depth_sheen_off">Off</string>
|
||||
<string name="settings_card_depth_sheen_soft">Soft</string>
|
||||
<string name="settings_card_depth_sheen_bright">Bright</string>
|
||||
<string name="settings_card_depth_apply_to">Apply to</string>
|
||||
<string name="settings_card_depth_fine_tune">Fine-tune</string>
|
||||
<string name="settings_card_depth_fine_tune_title">Fine-tune Depth</string>
|
||||
<string name="settings_card_depth_fine_tune_hint">Drag the dot right for a brighter edge, up for more sheen. Use the slider to extend the glow around the full outline.</string>
|
||||
<string name="settings_card_depth_pad_edge_axis">Edge glow →</string>
|
||||
<string name="settings_card_depth_pad_sheen_axis">↑ Sheen</string>
|
||||
<string name="settings_card_depth_edge_value">Edge glow</string>
|
||||
<string name="settings_card_depth_sheen_value">Sheen</string>
|
||||
<string name="settings_card_depth_edge_coverage">Edge coverage</string>
|
||||
<string name="settings_card_depth_coverage_top">Top only</string>
|
||||
<string name="settings_card_depth_coverage_half">Half</string>
|
||||
<string name="settings_card_depth_coverage_full">Full outline</string>
|
||||
<string name="settings_card_depth_coverage_value">Edge coverage</string>
|
||||
<string name="settings_card_depth_preview_title">Episode Title</string>
|
||||
<string name="settings_card_depth_preview_meta">S1 · E1 · 45 min</string>
|
||||
<string name="settings_card_depth_surface_posters">Posters</string>
|
||||
<string name="settings_card_depth_surface_continue_watching">Continue Watching</string>
|
||||
<string name="settings_card_depth_surface_episodes">Episode cards</string>
|
||||
<string name="settings_card_depth_surface_cast">Cast</string>
|
||||
<string name="settings_card_depth_surface_trailers">Trailers</string>
|
||||
<string name="settings_poster_card_radius">Corner Radius</string>
|
||||
<string name="settings_poster_card_style">Poster Card Style</string>
|
||||
<string name="settings_poster_card_width">Width</string>
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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()),
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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<CardDepthStyleUiState> = _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<StoredCardDepthStylePreferences>(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,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
package com.nuvio.app.core.ui
|
||||
|
||||
internal expect object CardDepthStyleStorage {
|
||||
fun loadPayload(): String?
|
||||
fun savePayload(payload: String)
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
) {
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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<Float, Float> {
|
||||
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<Float, Float> {
|
||||
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)
|
||||
|
|
|
|||
|
|
@ -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)),
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue