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 196fe2d..bfd0ebb 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,5 +1,11 @@ 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 @@ -17,6 +23,8 @@ 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 @@ -26,6 +34,42 @@ 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) + val animatedTotal by animateIntAsState( + targetValue = if (!state.isLoading) state.stats.totalPosts else 0, + animationSpec = tween(600), + label = "totalPosts" + ) + val animatedPublished by animateIntAsState( + targetValue = if (!state.isLoading) state.stats.publishedCount else 0, + animationSpec = tween(600), + label = "published" + ) + val animatedDrafts by animateIntAsState( + targetValue = if (!state.isLoading) state.stats.draftCount else 0, + animationSpec = tween(600), + label = "drafts" + ) + val animatedScheduled by animateIntAsState( + targetValue = if (!state.isLoading) state.stats.scheduledCount else 0, + animationSpec = tween(600), + label = "scheduled" + ) + Scaffold( topBar = { TopAppBar( @@ -65,36 +109,56 @@ fun StatsScreen( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(12.dp) ) { - StatsCard( + AnimatedVisibility( + visible = cardVisible[0].value, modifier = Modifier.weight(1f), - value = "${state.stats.totalPosts}", - label = "Total Posts", - icon = Icons.AutoMirrored.Filled.Article - ) - StatsCard( + 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), - value = "${state.stats.publishedCount}", - label = "Published", - icon = Icons.Default.Create - ) + 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) ) { - StatsCard( + AnimatedVisibility( + visible = cardVisible[2].value, modifier = Modifier.weight(1f), - value = "${state.stats.draftCount}", - label = "Drafts", - icon = Icons.Default.TextFields - ) - StatsCard( + 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), - value = "${state.stats.scheduledCount}", - label = "Scheduled", - icon = Icons.Default.Schedule - ) + enter = scaleIn(animationSpec = SwooshMotion.bouncy()) + fadeIn(SwooshMotion.quick()) + ) { + StatsCard( + value = "$animatedScheduled", + label = "Scheduled", + icon = Icons.Default.Schedule + ) + } } Spacer(modifier = Modifier.height(8.dp)) @@ -106,22 +170,27 @@ fun StatsScreen( fontWeight = FontWeight.SemiBold ) - 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") + 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") + } } }