diff --git a/app/src/main/java/com/swoosh/microblog/ui/stats/StatsScreen.kt b/app/src/main/java/com/swoosh/microblog/ui/stats/StatsScreen.kt index e700d7b..8ffc62e 100644 --- a/app/src/main/java/com/swoosh/microblog/ui/stats/StatsScreen.kt +++ b/app/src/main/java/com/swoosh/microblog/ui/stats/StatsScreen.kt @@ -1,11 +1,7 @@ package com.swoosh.microblog.ui.stats -import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.core.animateIntAsState import androidx.compose.animation.core.tween -import androidx.compose.animation.fadeIn -import androidx.compose.animation.scaleIn -import androidx.compose.animation.slideInVertically import androidx.compose.foundation.layout.* import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll @@ -22,8 +18,6 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.viewmodel.compose.viewModel -import com.swoosh.microblog.ui.animation.SwooshMotion -import kotlinx.coroutines.delay @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -32,38 +26,24 @@ fun StatsScreen( ) { val state by viewModel.uiState.collectAsStateWithLifecycle() - // Staggered entrance for stats cards - val cardVisible = remember { List(4) { mutableStateOf(false) } } - var writingStatsVisible by remember { mutableStateOf(false) } - LaunchedEffect(state.isLoading) { - if (!state.isLoading) { - cardVisible.forEachIndexed { index, vis -> - delay(SwooshMotion.StaggerDelayMs * index) - vis.value = true - } - delay(SwooshMotion.StaggerDelayMs * 4) - writingStatsVisible = true - } - } - - // Animated counters (ST3) — snappier 400ms count-up + // Animated counters — numbers count up from 0 when data loads val animatedTotal by animateIntAsState( - targetValue = if (!state.isLoading) state.stats.totalPosts else 0, + targetValue = state.stats.totalPosts, animationSpec = tween(400), label = "totalPosts" ) val animatedPublished by animateIntAsState( - targetValue = if (!state.isLoading) state.stats.publishedCount else 0, + targetValue = state.stats.publishedCount, animationSpec = tween(400), label = "published" ) val animatedDrafts by animateIntAsState( - targetValue = if (!state.isLoading) state.stats.draftCount else 0, + targetValue = state.stats.draftCount, animationSpec = tween(400), label = "drafts" ) val animatedScheduled by animateIntAsState( - targetValue = if (!state.isLoading) state.stats.scheduledCount else 0, + targetValue = state.stats.scheduledCount, animationSpec = tween(400), label = "scheduled" ) @@ -75,126 +55,92 @@ fun StatsScreen( ) } ) { padding -> - if (state.isLoading) { - Box( - modifier = Modifier.fillMaxSize().padding(padding), - contentAlignment = Alignment.Center + Column( + modifier = Modifier + .fillMaxSize() + .padding(padding) + .verticalScroll(rememberScrollState()) + .padding(16.dp), + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + // Post counts section + Text( + "Posts Overview", + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.SemiBold + ) + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(12.dp) ) { - CircularProgressIndicator() + StatsCard( + modifier = Modifier.weight(1f), + value = "$animatedTotal", + label = "Total Posts", + icon = Icons.AutoMirrored.Filled.Article + ) + StatsCard( + modifier = Modifier.weight(1f), + value = "$animatedPublished", + label = "Published", + icon = Icons.Default.Create + ) } - } else { - Column( - modifier = Modifier - .fillMaxSize() - .padding(padding) - .verticalScroll(rememberScrollState()) - .padding(16.dp), - verticalArrangement = Arrangement.spacedBy(16.dp) + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(12.dp) ) { - // Post counts section - Text( - "Posts Overview", - style = MaterialTheme.typography.titleMedium, - fontWeight = FontWeight.SemiBold + StatsCard( + modifier = Modifier.weight(1f), + value = "$animatedDrafts", + label = "Drafts", + icon = Icons.Default.TextFields ) + StatsCard( + modifier = Modifier.weight(1f), + value = "$animatedScheduled", + label = "Scheduled", + icon = Icons.Default.Schedule + ) + } - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy(12.dp) + Spacer(modifier = Modifier.height(8.dp)) + + // Writing stats section + Text( + "Writing Stats", + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.SemiBold + ) + + OutlinedCard(modifier = Modifier.fillMaxWidth()) { + Column( + modifier = Modifier.padding(16.dp), + verticalArrangement = Arrangement.spacedBy(12.dp) ) { - AnimatedVisibility( - visible = cardVisible[0].value, - modifier = Modifier.weight(1f), - enter = scaleIn(animationSpec = SwooshMotion.bouncy()) + fadeIn(SwooshMotion.quick()) - ) { - StatsCard( - value = "$animatedTotal", - label = "Total Posts", - icon = Icons.AutoMirrored.Filled.Article - ) - } - AnimatedVisibility( - visible = cardVisible[1].value, - modifier = Modifier.weight(1f), - enter = scaleIn(animationSpec = SwooshMotion.bouncy()) + fadeIn(SwooshMotion.quick()) - ) { - StatsCard( - value = "$animatedPublished", - label = "Published", - icon = Icons.Default.Create - ) - } - } - - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy(12.dp) - ) { - AnimatedVisibility( - visible = cardVisible[2].value, - modifier = Modifier.weight(1f), - enter = scaleIn(animationSpec = SwooshMotion.bouncy()) + fadeIn(SwooshMotion.quick()) - ) { - StatsCard( - value = "$animatedDrafts", - label = "Drafts", - icon = Icons.Default.TextFields - ) - } - AnimatedVisibility( - visible = cardVisible[3].value, - modifier = Modifier.weight(1f), - enter = scaleIn(animationSpec = SwooshMotion.bouncy()) + fadeIn(SwooshMotion.quick()) - ) { - StatsCard( - value = "$animatedScheduled", - label = "Scheduled", - icon = Icons.Default.Schedule - ) - } + WritingStatRow("Total words written", "${state.stats.totalWords}") + HorizontalDivider() + WritingStatRow("Total characters", "${state.stats.totalCharacters}") + HorizontalDivider() + WritingStatRow("Average post length", "${state.stats.averageWordCount} words") + HorizontalDivider() + WritingStatRow("Average characters", "${state.stats.averageCharCount} chars") + HorizontalDivider() + WritingStatRow("Longest post", "${state.stats.longestPostWords} words") + HorizontalDivider() + WritingStatRow("Shortest post", "${state.stats.shortestPostWords} words") } + } + if (state.error != null) { Spacer(modifier = Modifier.height(8.dp)) - - // Writing stats section Text( - "Writing Stats", - style = MaterialTheme.typography.titleMedium, - fontWeight = FontWeight.SemiBold + text = "Note: Remote post data may be incomplete. ${state.error}", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant ) - - AnimatedVisibility( - visible = writingStatsVisible, - enter = slideInVertically(initialOffsetY = { it / 3 }, animationSpec = SwooshMotion.gentle()) + fadeIn(SwooshMotion.quick()) - ) { - OutlinedCard(modifier = Modifier.fillMaxWidth()) { - Column( - modifier = Modifier.padding(16.dp), - verticalArrangement = Arrangement.spacedBy(12.dp) - ) { - WritingStatRow("Total words written", "${state.stats.totalWords}") - HorizontalDivider() - WritingStatRow("Total characters", "${state.stats.totalCharacters}") - HorizontalDivider() - WritingStatRow("Average post length", "${state.stats.averageWordCount} words") - HorizontalDivider() - WritingStatRow("Average characters", "${state.stats.averageCharCount} chars") - HorizontalDivider() - WritingStatRow("Longest post", "${state.stats.longestPostWords} words") - HorizontalDivider() - WritingStatRow("Shortest post", "${state.stats.shortestPostWords} words") - } - } - } - - if (state.error != null) { - Spacer(modifier = Modifier.height(8.dp)) - Text( - text = "Note: Remote post data may be incomplete. ${state.error}", - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } } } }