package com.craigvg.lichun_android.ui.components.indicators 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.material3.Text import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.scale import androidx.compose.ui.draw.shadow import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.StrokeCap import androidx.compose.ui.graphics.drawscope.Stroke import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp 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 /** * Linear progress bar with message and percentage * Ported from iOS ProgressLoadingView.swift */ @Composable fun ProgressLoadingView( message: String, progress: Float, // 0f to 1f modifier: Modifier = Modifier ) { val screenWidth = LocalConfiguration.current.screenWidthDp.dp val animatedProgress by animateFloatAsState( targetValue = progress, animationSpec = spring(dampingRatio = 0.8f, stiffness = Spring.StiffnessMediumLow), label = "progress" ) Column( modifier = modifier .clip(RoundedCornerShape(20.dp)) .shadow(12.dp, RoundedCornerShape(20.dp)) .background(AppColors.surfaceElevated) .padding(AppSpacing.xl), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(AppSpacing.lg) ) { // Progress bar Box( modifier = Modifier .fillMaxWidth() .height(12.dp) .clip(RoundedCornerShape(8.dp)) .background(AppColors.surfaceSubtle) ) { Box( modifier = Modifier .fillMaxWidth(animatedProgress.coerceIn(0f, 1f)) .fillMaxHeight() .clip(RoundedCornerShape(8.dp)) .background( Brush.horizontalGradient( colors = listOf(AppColors.primary, AppColors.accent) ) ) .shadow(4.dp, RoundedCornerShape(8.dp)) ) } Text( text = message, style = AppTypography.body, color = AppColors.primaryText, textAlign = TextAlign.Center ) Text( text = "${(progress * 100).toInt()}%", style = AppTypography.title.copy(fontSize = 24.sp), color = AppColors.primary ) } } /** * Circular progress indicator with percentage text */ @Composable fun CircularProgressView( progress: Float, // 0f to 1f modifier: Modifier = Modifier, size: Dp = 60.dp, strokeWidth: Dp = 6.dp, color: Color = AppColors.primary ) { val animatedProgress by animateFloatAsState( targetValue = progress, animationSpec = spring(dampingRatio = 0.8f, stiffness = Spring.StiffnessMediumLow), label = "circularProgress" ) Box( modifier = modifier.size(size), contentAlignment = Alignment.Center ) { androidx.compose.foundation.Canvas(modifier = Modifier.fillMaxSize()) { // Background circle drawArc( color = color.copy(alpha = 0.15f), startAngle = 0f, sweepAngle = 360f, useCenter = false, style = Stroke(width = strokeWidth.toPx(), cap = StrokeCap.Round) ) // Progress arc drawArc( brush = Brush.linearGradient( colors = listOf(color, color.copy(alpha = 0.7f)) ), startAngle = -90f, sweepAngle = animatedProgress * 360f, useCenter = false, style = Stroke(width = strokeWidth.toPx(), cap = StrokeCap.Round) ) } Text( text = "${(animatedProgress * 100).toInt()}%", style = AppTypography.caption.copy(fontSize = (size.value * 0.2f).sp), color = AppColors.primaryText ) } } /** * Animated loading dots */ @Composable fun LoadingDots( modifier: Modifier = Modifier, dotCount: Int = 3, color: Color = AppColors.primary ) { var currentDot by remember { mutableIntStateOf(0) } LaunchedEffect(Unit) { while (true) { delay(350) currentDot = (currentDot + 1) % dotCount } } Row( modifier = modifier, horizontalArrangement = Arrangement.spacedBy(AppSpacing.xs), verticalAlignment = Alignment.CenterVertically ) { repeat(dotCount) { index -> val scale by animateFloatAsState( targetValue = if (currentDot == index) 1.3f else 0.7f, animationSpec = spring(dampingRatio = 0.6f, stiffness = Spring.StiffnessMedium), label = "dotScale$index" ) val alpha by animateFloatAsState( targetValue = if (currentDot == index) 1f else 0.4f, animationSpec = spring(dampingRatio = 0.6f, stiffness = Spring.StiffnessMedium), label = "dotAlpha$index" ) Box( modifier = Modifier .size(10.dp) .scale(scale) .clip(CircleShape) .background(color.copy(alpha = alpha)) ) } } }