mirror of
https://github.com/pawelorzech/Swoosh.git
synced 2026-03-31 20:15:41 +00:00
fix: stats screen shows layout instantly, only numbers animate (no stagger entrance)
This commit is contained in:
parent
3da3e97e77
commit
4a2a18282c
1 changed files with 80 additions and 134 deletions
|
|
@ -1,11 +1,7 @@
|
||||||
package com.swoosh.microblog.ui.stats
|
package com.swoosh.microblog.ui.stats
|
||||||
|
|
||||||
import androidx.compose.animation.AnimatedVisibility
|
|
||||||
import androidx.compose.animation.core.animateIntAsState
|
import androidx.compose.animation.core.animateIntAsState
|
||||||
import androidx.compose.animation.core.tween
|
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.layout.*
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
|
@ -22,8 +18,6 @@ import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
import com.swoosh.microblog.ui.animation.SwooshMotion
|
|
||||||
import kotlinx.coroutines.delay
|
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
|
|
@ -32,38 +26,24 @@ fun StatsScreen(
|
||||||
) {
|
) {
|
||||||
val state by viewModel.uiState.collectAsStateWithLifecycle()
|
val state by viewModel.uiState.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
// Staggered entrance for stats cards
|
// Animated counters — numbers count up from 0 when data loads
|
||||||
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
|
|
||||||
val animatedTotal by animateIntAsState(
|
val animatedTotal by animateIntAsState(
|
||||||
targetValue = if (!state.isLoading) state.stats.totalPosts else 0,
|
targetValue = state.stats.totalPosts,
|
||||||
animationSpec = tween(400),
|
animationSpec = tween(400),
|
||||||
label = "totalPosts"
|
label = "totalPosts"
|
||||||
)
|
)
|
||||||
val animatedPublished by animateIntAsState(
|
val animatedPublished by animateIntAsState(
|
||||||
targetValue = if (!state.isLoading) state.stats.publishedCount else 0,
|
targetValue = state.stats.publishedCount,
|
||||||
animationSpec = tween(400),
|
animationSpec = tween(400),
|
||||||
label = "published"
|
label = "published"
|
||||||
)
|
)
|
||||||
val animatedDrafts by animateIntAsState(
|
val animatedDrafts by animateIntAsState(
|
||||||
targetValue = if (!state.isLoading) state.stats.draftCount else 0,
|
targetValue = state.stats.draftCount,
|
||||||
animationSpec = tween(400),
|
animationSpec = tween(400),
|
||||||
label = "drafts"
|
label = "drafts"
|
||||||
)
|
)
|
||||||
val animatedScheduled by animateIntAsState(
|
val animatedScheduled by animateIntAsState(
|
||||||
targetValue = if (!state.isLoading) state.stats.scheduledCount else 0,
|
targetValue = state.stats.scheduledCount,
|
||||||
animationSpec = tween(400),
|
animationSpec = tween(400),
|
||||||
label = "scheduled"
|
label = "scheduled"
|
||||||
)
|
)
|
||||||
|
|
@ -75,126 +55,92 @@ fun StatsScreen(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
) { padding ->
|
) { padding ->
|
||||||
if (state.isLoading) {
|
Column(
|
||||||
Box(
|
modifier = Modifier
|
||||||
modifier = Modifier.fillMaxSize().padding(padding),
|
.fillMaxSize()
|
||||||
contentAlignment = Alignment.Center
|
.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(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier.fillMaxWidth(),
|
||||||
.fillMaxSize()
|
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
||||||
.padding(padding)
|
|
||||||
.verticalScroll(rememberScrollState())
|
|
||||||
.padding(16.dp),
|
|
||||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
|
||||||
) {
|
) {
|
||||||
// Post counts section
|
StatsCard(
|
||||||
Text(
|
modifier = Modifier.weight(1f),
|
||||||
"Posts Overview",
|
value = "$animatedDrafts",
|
||||||
style = MaterialTheme.typography.titleMedium,
|
label = "Drafts",
|
||||||
fontWeight = FontWeight.SemiBold
|
icon = Icons.Default.TextFields
|
||||||
)
|
)
|
||||||
|
StatsCard(
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
value = "$animatedScheduled",
|
||||||
|
label = "Scheduled",
|
||||||
|
icon = Icons.Default.Schedule
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
Row(
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(12.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(
|
WritingStatRow("Total words written", "${state.stats.totalWords}")
|
||||||
visible = cardVisible[0].value,
|
HorizontalDivider()
|
||||||
modifier = Modifier.weight(1f),
|
WritingStatRow("Total characters", "${state.stats.totalCharacters}")
|
||||||
enter = scaleIn(animationSpec = SwooshMotion.bouncy()) + fadeIn(SwooshMotion.quick())
|
HorizontalDivider()
|
||||||
) {
|
WritingStatRow("Average post length", "${state.stats.averageWordCount} words")
|
||||||
StatsCard(
|
HorizontalDivider()
|
||||||
value = "$animatedTotal",
|
WritingStatRow("Average characters", "${state.stats.averageCharCount} chars")
|
||||||
label = "Total Posts",
|
HorizontalDivider()
|
||||||
icon = Icons.AutoMirrored.Filled.Article
|
WritingStatRow("Longest post", "${state.stats.longestPostWords} words")
|
||||||
)
|
HorizontalDivider()
|
||||||
}
|
WritingStatRow("Shortest post", "${state.stats.shortestPostWords} words")
|
||||||
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
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state.error != null) {
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
|
||||||
// Writing stats section
|
|
||||||
Text(
|
Text(
|
||||||
"Writing Stats",
|
text = "Note: Remote post data may be incomplete. ${state.error}",
|
||||||
style = MaterialTheme.typography.titleMedium,
|
style = MaterialTheme.typography.bodySmall,
|
||||||
fontWeight = FontWeight.SemiBold
|
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
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue