mirror of
https://github.com/pawelorzech/Swoosh.git
synced 2026-03-31 11:55:47 +00:00
feat: add staggered stats cards and count-up animations
This commit is contained in:
parent
188c62f076
commit
a6429f16d3
1 changed files with 105 additions and 36 deletions
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue