diff --git a/app/src/main/java/com/fluxa/app/ui/catalog/FluxaDimensions.kt b/app/src/main/java/com/fluxa/app/ui/catalog/FluxaDimensions.kt index 9fc8514..1f3a925 100644 --- a/app/src/main/java/com/fluxa/app/ui/catalog/FluxaDimensions.kt +++ b/app/src/main/java/com/fluxa/app/ui/catalog/FluxaDimensions.kt @@ -77,7 +77,6 @@ object FluxaDimensions { // ── Profile screens ─────────────────────────────────────────────────────── object Profile { val avatarSize = 120.dp - val avatarPickerThumbSize = 80.dp } // ── Card text sizes ─────────────────────────────────────────────────────── @@ -111,6 +110,10 @@ object FluxaDimensions { const val heightAnim = 130 const val settingsExpand = 240 const val settingsExpandAlt = 260 + const val heroReveal = 650 + const val fadeIn = 200 + const val fadeOut = 150 + const val ambientColor = 700 } // ── Focus indicator stroke ──────────────────────────────────────────────── diff --git a/app/src/main/java/com/fluxa/app/ui/catalog/ThemeHelper.kt b/app/src/main/java/com/fluxa/app/ui/catalog/ThemeHelper.kt index 40a96c8..b4787cd 100644 --- a/app/src/main/java/com/fluxa/app/ui/catalog/ThemeHelper.kt +++ b/app/src/main/java/com/fluxa/app/ui/catalog/ThemeHelper.kt @@ -33,7 +33,7 @@ fun rememberAmbientHeroColor(artworkUrl: String?): Color { value = ThemeHelper.extractColors(context, artworkUrl) } val target = lerp(colors.darkMuted, FluxaColors.background, 0.35f) - return animateColorAsState(target, tween(700), label = "ambientHero").value + return animateColorAsState(target, tween(FluxaDimensions.AnimDuration.ambientColor), label = "ambientHero").value } object ThemeHelper { diff --git a/app/src/main/java/com/fluxa/app/ui/catalog/TvHomeHeroComponents.kt b/app/src/main/java/com/fluxa/app/ui/catalog/TvHomeHeroComponents.kt index 7cecc0c..805a3c6 100644 --- a/app/src/main/java/com/fluxa/app/ui/catalog/TvHomeHeroComponents.kt +++ b/app/src/main/java/com/fluxa/app/ui/catalog/TvHomeHeroComponents.kt @@ -18,8 +18,11 @@ import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.animation.core.Animatable import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.core.tween +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.remember @@ -284,6 +287,13 @@ private fun youtubeEmbedHtml(youtubeId: String): String { """.trimIndent() } +private fun Modifier.heroReveal(progress: () -> Float, step: Int): Modifier = graphicsLayer { + val start = step * 0.15f + val f = ((progress() - start) / 0.55f).coerceIn(0f, 1f) + alpha = f + translationY = (1f - f) * 22.dp.toPx() +} + @Composable internal fun HomeHeroPanel( movie: Meta, @@ -300,12 +310,16 @@ internal fun HomeHeroPanel( ) { val buttonsAlpha by animateFloatAsState( targetValue = if (showButtons) 1f else 0f, - animationSpec = tween(220), + animationSpec = tween(FluxaDimensions.AnimDuration.contentExpand), label = "heroButtonsAlpha" ) val logoCandidates = remember(movie.id, movie.logo, logoUrl) { listOfNotNull(logoUrl, movie.logo).distinct() } + val reveal = remember(movie.id) { Animatable(0f) } + LaunchedEffect(movie.id) { + reveal.animateTo(1f, tween(FluxaDimensions.AnimDuration.heroReveal)) + } androidx.compose.foundation.layout.Column(modifier = modifier.widthIn(max = 560.dp)) { if (logoCandidates.isNotEmpty()) { @@ -314,7 +328,8 @@ internal fun HomeHeroPanel( contentDescription = movie.name, modifier = Modifier .height(94.dp) - .widthIn(max = 360.dp), + .widthIn(max = 360.dp) + .heroReveal({ reveal.value }, 0), contentScale = ContentScale.Fit ) } else { @@ -323,20 +338,26 @@ internal fun HomeHeroPanel( color = Color(0xFFFFD94B), fontSize = 54.sp, lineHeight = 56.sp, + fontFamily = FluxaDisplay, fontWeight = FontWeight.Black, style = TextStyle( shadow = Shadow( color = Color.Black.copy(alpha = 0.55f), blurRadius = 20f ) - ) + ), + modifier = Modifier.heroReveal({ reveal.value }, 0) ) } Spacer(modifier = Modifier.height(18.dp)) - HeroMetaLine(movie = movie, lang = lang) - Spacer(modifier = Modifier.height(10.dp)) - HeroRatingLine(movie = movie) + Box(modifier = Modifier.heroReveal({ reveal.value }, 1)) { + androidx.compose.foundation.layout.Column { + HeroMetaLine(movie = movie, lang = lang) + Spacer(modifier = Modifier.height(10.dp)) + HeroRatingLine(movie = movie) + } + } Spacer(modifier = Modifier.height(18.dp)) if (!movie.description.isNullOrBlank()) { @@ -347,14 +368,18 @@ internal fun HomeHeroPanel( lineHeight = 27.sp, maxLines = 4, overflow = TextOverflow.Ellipsis, - modifier = Modifier.widthIn(max = 500.dp) + modifier = Modifier + .widthIn(max = 500.dp) + .heroReveal({ reveal.value }, 2) ) Spacer(modifier = Modifier.height(22.dp)) } Row( horizontalArrangement = Arrangement.spacedBy(14.dp), - modifier = Modifier.alpha(buttonsAlpha) + modifier = Modifier + .alpha(buttonsAlpha) + .heroReveal({ reveal.value }, 3) ) { Button( onClick = onPlayClick, diff --git a/app/src/main/java/com/fluxa/app/ui/catalog/TvHomeShelfComponents.kt b/app/src/main/java/com/fluxa/app/ui/catalog/TvHomeShelfComponents.kt index 0beb939..74e19e1 100644 --- a/app/src/main/java/com/fluxa/app/ui/catalog/TvHomeShelfComponents.kt +++ b/app/src/main/java/com/fluxa/app/ui/catalog/TvHomeShelfComponents.kt @@ -117,7 +117,7 @@ private fun WideContinueCard( onFocus: () -> Unit ) { var isFocused by remember { mutableStateOf(false) } - val scale by animateFloatAsState(if (isFocused) 1.035f else 1f, tween(180), label = "continueScale") + val scale by animateFloatAsState(if (isFocused) 1.035f else 1f, tween(FluxaDimensions.AnimDuration.scaleAlpha), label = "continueScale") Row( modifier = Modifier @@ -353,12 +353,12 @@ internal fun HomeShelfRow( @Composable private fun ShelfExpandedDescription(expandedMeta: Meta?, expandedOffsetX: Dp, rowWidth: Dp) { - val animatedOffsetX by animateDpAsState(expandedOffsetX, tween(220), label = "expandedInfoOffset") + val animatedOffsetX by animateDpAsState(expandedOffsetX, tween(FluxaDimensions.AnimDuration.contentExpand), label = "expandedInfoOffset") Box(modifier = Modifier.fillMaxWidth().height(90.dp)) { AnimatedVisibility( visible = expandedMeta != null, - enter = fadeIn(tween(200)), - exit = fadeOut(tween(150)) + enter = fadeIn(tween(FluxaDimensions.AnimDuration.fadeIn)), + exit = fadeOut(tween(FluxaDimensions.AnimDuration.fadeOut)) ) { val info = expandedMeta if (info != null) { diff --git a/app/src/main/java/com/fluxa/app/ui/catalog/TvMovieCard.kt b/app/src/main/java/com/fluxa/app/ui/catalog/TvMovieCard.kt index d5fe07a..87e08b4 100644 --- a/app/src/main/java/com/fluxa/app/ui/catalog/TvMovieCard.kt +++ b/app/src/main/java/com/fluxa/app/ui/catalog/TvMovieCard.kt @@ -77,7 +77,7 @@ internal fun TvMovieCard( val radius = posterCornerRadius(profile?.safeCardCornerPreset ?: "soft") val animationDuration = when { profile?.safeAnimationsEnabled == false -> 0 - else -> if (isShelfStyle) 220 else 240 + else -> if (isShelfStyle) FluxaDimensions.AnimDuration.contentExpand else FluxaDimensions.AnimDuration.cardFocusScale } val hidePosterTitles = profile?.safePosterHideTitles == true || meta.hideTitle == true