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 457eb87..e4ad5ee 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,24 +1,34 @@ package com.swoosh.microblog.ui.stats +import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.core.animateIntAsState import androidx.compose.animation.core.tween +import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.Article import androidx.compose.material.icons.filled.Create import androidx.compose.material.icons.filled.Schedule import androidx.compose.material.icons.filled.Refresh +import androidx.compose.material.icons.filled.Tag import androidx.compose.material.icons.filled.TextFields import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.viewmodel.compose.viewModel +import com.swoosh.microblog.data.model.GhostTagFull +import com.swoosh.microblog.ui.tags.parseHexColor @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -27,7 +37,7 @@ fun StatsScreen( ) { val state by viewModel.uiState.collectAsStateWithLifecycle() - // Animated counters — numbers count up from 0 when data loads + // Animated counters -- numbers count up from 0 when data loads val animatedTotal by animateIntAsState( targetValue = state.stats.totalPosts, animationSpec = tween(400), @@ -114,6 +124,57 @@ fun StatsScreen( Spacer(modifier = Modifier.height(8.dp)) + // Tags section + if (state.tagStats.isNotEmpty()) { + Text( + "Tags", + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.SemiBold + ) + + OutlinedCard(modifier = Modifier.fillMaxWidth()) { + Column( + modifier = Modifier.padding(16.dp), + verticalArrangement = Arrangement.spacedBy(12.dp) + ) { + // Most used tag + if (state.mostUsedTag != null) { + WritingStatRow( + "Most used tag", + "#${state.mostUsedTag!!.name} (${state.mostUsedTag!!.count?.posts ?: 0})" + ) + HorizontalDivider() + } + + // Posts without tags + WritingStatRow( + "Posts without tags", + "${state.postsWithoutTags}" + ) + + HorizontalDivider() + + // Total tags + WritingStatRow( + "Total tags", + "${state.tagStats.size}" + ) + + // Tag progress bars + val maxCount = state.tagStats.maxOfOrNull { it.count?.posts ?: 0 } ?: 1 + + state.tagStats.take(10).forEach { tag -> + TagProgressBar( + tag = tag, + maxCount = maxCount.coerceAtLeast(1) + ) + } + } + } + + Spacer(modifier = Modifier.height(8.dp)) + } + // Writing stats section Text( "Writing Stats", @@ -152,6 +213,68 @@ fun StatsScreen( } } +@Composable +private fun TagProgressBar( + tag: GhostTagFull, + maxCount: Int +) { + val postCount = tag.count?.posts ?: 0 + val progress = postCount.toFloat() / maxCount.toFloat() + val animatedProgress by animateFloatAsState( + targetValue = progress, + animationSpec = tween(600), + label = "tagProgress_${tag.name}" + ) + + val barColor = tag.accent_color?.let { parseHexColor(it) } + ?: MaterialTheme.colorScheme.primary + + Column(modifier = Modifier.fillMaxWidth()) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Row( + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.weight(1f) + ) { + Box( + modifier = Modifier + .size(8.dp) + .clip(CircleShape) + .background(barColor) + ) + Text( + text = tag.name, + style = MaterialTheme.typography.bodySmall, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } + Text( + text = "$postCount", + style = MaterialTheme.typography.labelMedium, + fontWeight = FontWeight.SemiBold, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + + Spacer(modifier = Modifier.height(4.dp)) + + LinearProgressIndicator( + progress = { animatedProgress }, + modifier = Modifier + .fillMaxWidth() + .height(6.dp) + .clip(RoundedCornerShape(3.dp)), + color = barColor, + trackColor = MaterialTheme.colorScheme.surfaceVariant + ) + } +} + @Composable private fun StatsCard( modifier: Modifier = Modifier, diff --git a/app/src/main/java/com/swoosh/microblog/ui/stats/StatsViewModel.kt b/app/src/main/java/com/swoosh/microblog/ui/stats/StatsViewModel.kt index 790a7c1..51fd305 100644 --- a/app/src/main/java/com/swoosh/microblog/ui/stats/StatsViewModel.kt +++ b/app/src/main/java/com/swoosh/microblog/ui/stats/StatsViewModel.kt @@ -4,8 +4,10 @@ import android.app.Application import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.viewModelScope import com.swoosh.microblog.data.model.FeedPost +import com.swoosh.microblog.data.model.GhostTagFull import com.swoosh.microblog.data.model.OverallStats import com.swoosh.microblog.data.repository.PostRepository +import com.swoosh.microblog.data.repository.TagRepository import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow @@ -15,6 +17,7 @@ import kotlinx.coroutines.launch class StatsViewModel(application: Application) : AndroidViewModel(application) { private val repository = PostRepository(application) + private val tagRepository = TagRepository(application) private val _uiState = MutableStateFlow(StatsUiState()) val uiState: StateFlow = _uiState.asStateFlow() @@ -50,6 +53,7 @@ class StatsViewModel(application: Application) : AndroidViewModel(application) { linkTitle = null, linkDescription = null, linkImageUrl = null, + tags = ghost.tags?.map { it.name } ?: emptyList(), status = ghost.status ?: "draft", publishedAt = ghost.published_at, createdAt = ghost.created_at, @@ -73,7 +77,33 @@ class StatsViewModel(application: Application) : AndroidViewModel(application) { val uniqueRemotePosts = remotePosts.filter { it.ghostId !in localGhostIds } val stats = OverallStats.calculate(localPosts, uniqueRemotePosts) - _uiState.update { it.copy(stats = stats, isLoading = false) } + + // Fetch tag stats + val tagStats = try { + tagRepository.fetchTags().getOrNull() + ?.sortedByDescending { it.count?.posts ?: 0 } + ?: emptyList() + } catch (e: Exception) { + emptyList() + } + + // Count posts without any tags + val totalPosts = localPosts.size + uniqueRemotePosts.size + val postsWithTags = uniqueRemotePosts.count { it.tags.isNotEmpty() } + val postsWithoutTags = totalPosts - postsWithTags + + // Most used tag + val mostUsedTag = tagStats.firstOrNull() + + _uiState.update { + it.copy( + stats = stats, + tagStats = tagStats, + mostUsedTag = mostUsedTag, + postsWithoutTags = postsWithoutTags, + isLoading = false + ) + } } catch (e: Exception) { _uiState.update { it.copy(isLoading = false, error = e.message) } } @@ -83,6 +113,9 @@ class StatsViewModel(application: Application) : AndroidViewModel(application) { data class StatsUiState( val stats: OverallStats = OverallStats(), + val tagStats: List = emptyList(), + val mostUsedTag: GhostTagFull? = null, + val postsWithoutTags: Int = 0, val isLoading: Boolean = false, val error: String? = null )