package com.craigvg.lichun_android.ui.screens.retention import androidx.compose.animation.core.* import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Star import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.rotate import androidx.compose.ui.draw.scale import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color 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 import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.DialogProperties import com.craigvg.lichun_android.domain.models.Achievement import com.craigvg.lichun_android.domain.models.AchievementCategory import com.craigvg.lichun_android.ui.theme.AppColors import com.craigvg.lichun_android.ui.theme.AppSpacing import com.craigvg.lichun_android.ui.theme.AppTypography import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlin.math.cos import kotlin.math.sin import kotlin.random.Random /** * Celebration modal shown when achievements are unlocked * Ported from iOS AchievementUnlockModal.swift */ @Composable fun AchievementUnlockModal( achievements: List, onAcknowledge: (String) -> Unit, onDismiss: () -> Unit ) { var currentIndex by remember { mutableIntStateOf(0) } var showConfetti by remember { mutableStateOf(true) } val currentAchievement = achievements.getOrNull(currentIndex) // Animation states var scale by remember { mutableFloatStateOf(0.5f) } var rotation by remember { mutableFloatStateOf(-15f) } var cardOffsetY by remember { mutableFloatStateOf(500f) } // Sparkle animation val infiniteTransition = rememberInfiniteTransition(label = "sparkle") val sparkleOpacity by infiniteTransition.animateFloat( initialValue = 0f, targetValue = 1f, animationSpec = infiniteRepeatable( animation = tween(800, easing = EaseInOut), repeatMode = RepeatMode.Reverse ), label = "sparkleOpacity" ) val pulseScale by infiniteTransition.animateFloat( initialValue = 1f, targetValue = 1.2f, animationSpec = infiniteRepeatable( animation = tween(1500, easing = EaseInOut), repeatMode = RepeatMode.Reverse ), label = "pulseScale" ) // Entrance animation LaunchedEffect(currentIndex) { scale = 0.5f rotation = -15f cardOffsetY = 500f showConfetti = true // Animate in (run animations concurrently) launch { animate( initialValue = 500f, targetValue = 0f, animationSpec = spring( dampingRatio = Spring.DampingRatioMediumBouncy, stiffness = Spring.StiffnessLow ) ) { value, _ -> cardOffsetY = value } } launch { animate( initialValue = 0.5f, targetValue = 1f, animationSpec = spring( dampingRatio = Spring.DampingRatioMediumBouncy, stiffness = Spring.StiffnessLow ) ) { value, _ -> scale = value } } launch { animate( initialValue = -15f, targetValue = 0f, animationSpec = spring( dampingRatio = Spring.DampingRatioMediumBouncy, stiffness = Spring.StiffnessLow ) ) { value, _ -> rotation = value } } // Hide confetti after 3 seconds delay(3000) showConfetti = false } if (currentAchievement != null) { Dialog( onDismissRequest = onDismiss, properties = DialogProperties( dismissOnBackPress = true, dismissOnClickOutside = true ) ) { Box( modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center ) { // Confetti if (showConfetti) { ConfettiAnimation() } Column( modifier = Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally ) { // Achievement Card Card( modifier = Modifier .fillMaxWidth() .padding(AppSpacing.lg) .offset(y = cardOffsetY.dp) .scale(scale) .rotate(rotation), shape = RoundedCornerShape(AppSpacing.largeCornerRadius), colors = CardDefaults.cardColors( containerColor = Color.Transparent ), elevation = CardDefaults.cardElevation(defaultElevation = 20.dp) ) { Box( modifier = Modifier .background( Brush.linearGradient( colors = listOf( getCategoryColor(currentAchievement.category).copy(alpha = 0.9f), getCategoryColor(currentAchievement.category).copy(alpha = 0.7f) ) ) ) .padding(AppSpacing.xl) ) { Column( horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(AppSpacing.lg) ) { // Stars and trophy Row( horizontalArrangement = Arrangement.spacedBy(12.dp), verticalAlignment = Alignment.CenterVertically ) { Text( text = "\u2B50", fontSize = 50.sp, modifier = Modifier.scale(scale) ) Text( text = "\uD83C\uDFC6", fontSize = 60.sp, modifier = Modifier.scale(scale) ) Text( text = "\u2B50", fontSize = 50.sp, modifier = Modifier.scale(scale) ) } Text( text = "Achievement Unlocked!", fontSize = 28.sp, fontWeight = FontWeight.Bold, color = Color.White ) // Achievement icon with animations Box( contentAlignment = Alignment.Center, modifier = Modifier.scale(scale) ) { // Pulsing ring Box( modifier = Modifier .size(120.dp) .scale(pulseScale) .clip(CircleShape) .background( getCategoryColor(currentAchievement.category).copy(alpha = 0.3f) ) ) // Inner circle Box( modifier = Modifier .size(100.dp) .clip(CircleShape) .background( getCategoryColor(currentAchievement.category).copy(alpha = 0.3f) ), contentAlignment = Alignment.Center ) { Text( text = "\uD83C\uDFC6", fontSize = 50.sp ) } // Sparkles repeat(8) { index -> val angle = index * 45.0 val x = (cos(Math.toRadians(angle)) * 60).toFloat() val y = (sin(Math.toRadians(angle)) * 60).toFloat() Text( text = "\u2728", fontSize = 12.sp, modifier = Modifier .offset(x = x.dp, y = y.dp) .alpha(sparkleOpacity) ) } } // Achievement name Text( text = currentAchievement.name, style = AppTypography.title, fontWeight = FontWeight.SemiBold, color = Color.White, textAlign = TextAlign.Center ) // Description Text( text = currentAchievement.description, style = AppTypography.body, color = Color.White.copy(alpha = 0.9f), textAlign = TextAlign.Center ) // Reward badge Box( modifier = Modifier .clip(RoundedCornerShape(AppSpacing.cornerRadius)) .background( Brush.linearGradient( colors = listOf( Color.White.copy(alpha = 0.25f), Color.White.copy(alpha = 0.15f) ) ) ) .padding(horizontal = AppSpacing.lg, vertical = AppSpacing.md) ) { Row( horizontalArrangement = Arrangement.spacedBy(AppSpacing.sm), verticalAlignment = Alignment.CenterVertically ) { Icon( imageVector = Icons.Default.Star, contentDescription = null, tint = Color(0xFFFFD700), modifier = Modifier.size(20.dp) ) Text( text = "You earned", style = AppTypography.body, color = Color.White.copy(alpha = 0.9f) ) Text( text = "${currentAchievement.reward}", fontSize = 28.sp, fontWeight = FontWeight.Bold, color = Color(0xFFFFD700) ) Text( text = "\uD83D\uDC8E", fontSize = 22.sp ) } } } } } Spacer(modifier = Modifier.height(AppSpacing.lg)) // Continue button Button( onClick = { onAcknowledge(currentAchievement.id) if (currentIndex < achievements.size - 1) { currentIndex++ } else { onDismiss() } }, modifier = Modifier .fillMaxWidth() .padding(horizontal = AppSpacing.xl) .height(AppSpacing.buttonHeight), shape = RoundedCornerShape(AppSpacing.cornerRadius), colors = ButtonDefaults.buttonColors(containerColor = Color.White), elevation = ButtonDefaults.buttonElevation(defaultElevation = 8.dp) ) { Text( text = if (currentIndex < achievements.size - 1) "Next" else "Awesome!", style = AppTypography.headline, color = AppColors.primaryText ) } } } } } } @Composable private fun ConfettiAnimation() { val confettiColors = listOf( Color(0xFFFFD700), // Gold Color(0xFFFFA500), // Orange-gold Color(0xFFD9A621), // Bronze Color(0xFFFFEB3B), // Bright yellow Color(0xFFFF8000), // Warm orange Color(0xFFCC8033), // Copper Color(0xFFFFC000), // Amber Color(0xFFFACC66) // Light gold ) val pieces = remember { List(50) { ConfettiPiece( initialX = Random.nextFloat() * 400f - 200f, color = confettiColors.random(), delay = Random.nextFloat() * 500f ) } } Box(modifier = Modifier.fillMaxSize()) { pieces.forEach { piece -> AnimatedConfettiPiece(piece = piece) } } } private data class ConfettiPiece( val initialX: Float, val color: Color, val delay: Float ) @Composable private fun AnimatedConfettiPiece(piece: ConfettiPiece) { var yOffset by remember { mutableFloatStateOf(-100f) } var xOffset by remember { mutableFloatStateOf(piece.initialX) } var rotation by remember { mutableFloatStateOf(0f) } var opacity by remember { mutableFloatStateOf(1f) } LaunchedEffect(Unit) { delay(piece.delay.toLong()) val duration = Random.nextFloat() * 2000 + 2000 launch { animate( initialValue = -100f, targetValue = 1000f, animationSpec = tween(duration.toInt(), easing = LinearEasing) ) { value, _ -> yOffset = value } } launch { animate( initialValue = piece.initialX, targetValue = piece.initialX + Random.nextFloat() * 100 - 50, animationSpec = tween(duration.toInt(), easing = LinearEasing) ) { value, _ -> xOffset = value } } launch { animate( initialValue = 0f, targetValue = Random.nextFloat() * 720, animationSpec = tween(duration.toInt(), easing = LinearEasing) ) { value, _ -> rotation = value } } launch { animate( initialValue = 1f, targetValue = 0f, animationSpec = tween(duration.toInt(), easing = LinearEasing) ) { value, _ -> opacity = value } } } Box( modifier = Modifier .offset(x = xOffset.dp, y = yOffset.dp) .rotate(rotation) .size(10.dp) .clip(RoundedCornerShape(3.dp)) .background(piece.color.copy(alpha = opacity)) ) } private fun getCategoryColor(category: AchievementCategory): Color { return category.color }