mirror of
https://github.com/mihonapp/mihon.git
synced 2025-11-13 04:28:55 +01:00
Implement simple stats screen (#8068)
* Implement simple stats screen * Review Changes * Some other changes * Remove unused * Small changes * Review Changes 2 + Cleanup * Review Changes 3 * Cleanup leftovers * Optimize imports
This commit is contained in:
@@ -31,7 +31,7 @@ fun LanguageBadge(
|
||||
) {
|
||||
if (isLocal) {
|
||||
Badge(
|
||||
text = stringResource(R.string.local_source_badge),
|
||||
text = stringResource(R.string.label_local),
|
||||
color = MaterialTheme.colorScheme.tertiary,
|
||||
textColor = MaterialTheme.colorScheme.onTertiary,
|
||||
)
|
||||
|
||||
@@ -292,7 +292,7 @@ private fun FilterPage(
|
||||
.verticalScroll(rememberScrollState()),
|
||||
) {
|
||||
FilterPageItem(
|
||||
label = stringResource(R.string.action_filter_downloaded),
|
||||
label = stringResource(R.string.label_downloaded),
|
||||
state = downloadFilter,
|
||||
onClick = onDownloadFilterChanged,
|
||||
)
|
||||
|
||||
@@ -270,7 +270,7 @@ private fun SearchResultItem(
|
||||
}
|
||||
if (startDate.isNotBlank()) {
|
||||
SearchResultItemDetails(
|
||||
title = stringResource(R.string.track_start_date),
|
||||
title = stringResource(R.string.label_started),
|
||||
text = startDate,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import androidx.compose.material.icons.outlined.GetApp
|
||||
import androidx.compose.material.icons.outlined.HelpOutline
|
||||
import androidx.compose.material.icons.outlined.Info
|
||||
import androidx.compose.material.icons.outlined.Label
|
||||
import androidx.compose.material.icons.outlined.QueryStats
|
||||
import androidx.compose.material.icons.outlined.Settings
|
||||
import androidx.compose.material.icons.outlined.SettingsBackupRestore
|
||||
import androidx.compose.runtime.Composable
|
||||
@@ -41,6 +42,7 @@ fun MoreScreen(
|
||||
isFDroid: Boolean,
|
||||
onClickDownloadQueue: () -> Unit,
|
||||
onClickCategories: () -> Unit,
|
||||
onClickStats: () -> Unit,
|
||||
onClickBackupAndRestore: () -> Unit,
|
||||
onClickSettings: () -> Unit,
|
||||
onClickAbout: () -> Unit,
|
||||
@@ -132,6 +134,13 @@ fun MoreScreen(
|
||||
onPreferenceClick = onClickCategories,
|
||||
)
|
||||
}
|
||||
item {
|
||||
TextPreferenceWidget(
|
||||
title = stringResource(R.string.label_stats),
|
||||
icon = Icons.Outlined.QueryStats,
|
||||
onPreferenceClick = onClickStats,
|
||||
)
|
||||
}
|
||||
item {
|
||||
TextPreferenceWidget(
|
||||
title = stringResource(R.string.label_backup),
|
||||
|
||||
@@ -0,0 +1,159 @@
|
||||
package eu.kanade.presentation.more.stats
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.CollectionsBookmark
|
||||
import androidx.compose.material.icons.outlined.LocalLibrary
|
||||
import androidx.compose.material.icons.outlined.Schedule
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import eu.kanade.core.util.toDurationString
|
||||
import eu.kanade.presentation.components.LazyColumn
|
||||
import eu.kanade.presentation.more.stats.components.StatsItem
|
||||
import eu.kanade.presentation.more.stats.components.StatsOverviewItem
|
||||
import eu.kanade.presentation.more.stats.components.StatsSection
|
||||
import eu.kanade.presentation.more.stats.data.StatsData
|
||||
import eu.kanade.presentation.util.padding
|
||||
import eu.kanade.tachiyomi.R
|
||||
import java.util.Locale
|
||||
import kotlin.time.DurationUnit
|
||||
import kotlin.time.toDuration
|
||||
|
||||
@Composable
|
||||
fun StatsScreenContent(
|
||||
state: StatsScreenState.Success,
|
||||
paddingValues: PaddingValues,
|
||||
) {
|
||||
val statListState = rememberLazyListState()
|
||||
LazyColumn(
|
||||
state = statListState,
|
||||
contentPadding = paddingValues,
|
||||
verticalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small),
|
||||
) {
|
||||
item {
|
||||
OverviewSection(state.overview)
|
||||
}
|
||||
item {
|
||||
TitlesStats(state.titles)
|
||||
}
|
||||
item {
|
||||
ChapterStats(state.chapters)
|
||||
}
|
||||
item {
|
||||
TrackerStats(state.trackers)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun OverviewSection(
|
||||
data: StatsData.Overview,
|
||||
) {
|
||||
val none = stringResource(R.string.none)
|
||||
val context = LocalContext.current
|
||||
val readDurationString = remember(data.totalReadDuration) {
|
||||
data.totalReadDuration
|
||||
.toDuration(DurationUnit.MILLISECONDS)
|
||||
.toDurationString(context, fallback = none)
|
||||
}
|
||||
StatsSection(R.string.label_overview_section) {
|
||||
Row {
|
||||
StatsOverviewItem(
|
||||
title = data.libraryMangaCount.toString(),
|
||||
subtitle = stringResource(R.string.in_library),
|
||||
icon = Icons.Outlined.CollectionsBookmark,
|
||||
)
|
||||
StatsOverviewItem(
|
||||
title = data.completedMangaCount.toString(),
|
||||
subtitle = stringResource(R.string.label_completed_titles),
|
||||
icon = Icons.Outlined.LocalLibrary,
|
||||
)
|
||||
StatsOverviewItem(
|
||||
title = readDurationString,
|
||||
subtitle = stringResource(R.string.label_read_duration),
|
||||
icon = Icons.Outlined.Schedule,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TitlesStats(
|
||||
data: StatsData.Titles,
|
||||
) {
|
||||
StatsSection(R.string.label_titles_section) {
|
||||
Row {
|
||||
StatsItem(
|
||||
data.globalUpdateItemCount.toString(),
|
||||
stringResource(R.string.label_titles_in_global_update),
|
||||
)
|
||||
StatsItem(
|
||||
data.startedMangaCount.toString(),
|
||||
stringResource(R.string.label_started),
|
||||
)
|
||||
StatsItem(
|
||||
data.localMangaCount.toString(),
|
||||
stringResource(R.string.label_local),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ChapterStats(
|
||||
data: StatsData.Chapters,
|
||||
) {
|
||||
StatsSection(R.string.chapters) {
|
||||
Row {
|
||||
StatsItem(
|
||||
data.totalChapterCount.toString(),
|
||||
stringResource(R.string.label_total_chapters),
|
||||
)
|
||||
StatsItem(
|
||||
data.readChapterCount.toString(),
|
||||
stringResource(R.string.label_read_chapters),
|
||||
)
|
||||
StatsItem(
|
||||
data.downloadCount.toString(),
|
||||
stringResource(R.string.label_downloaded),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TrackerStats(
|
||||
data: StatsData.Trackers,
|
||||
) {
|
||||
val notApplicable = stringResource(R.string.not_applicable)
|
||||
val meanScoreStr = remember(data.trackedTitleCount, data.meanScore) {
|
||||
if (data.trackedTitleCount > 0 && !data.meanScore.isNaN()) {
|
||||
// All other numbers are localized in English
|
||||
String.format(Locale.ENGLISH, "%.2f ★", data.meanScore)
|
||||
} else {
|
||||
notApplicable
|
||||
}
|
||||
}
|
||||
StatsSection(R.string.label_tracker_section) {
|
||||
Row {
|
||||
StatsItem(
|
||||
data.trackedTitleCount.toString(),
|
||||
stringResource(R.string.label_tracked_titles),
|
||||
)
|
||||
StatsItem(
|
||||
meanScoreStr,
|
||||
stringResource(R.string.label_mean_score),
|
||||
)
|
||||
StatsItem(
|
||||
data.trackerCount.toString(),
|
||||
stringResource(R.string.label_used),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package eu.kanade.presentation.more.stats
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import eu.kanade.presentation.more.stats.data.StatsData
|
||||
|
||||
sealed class StatsScreenState {
|
||||
@Immutable
|
||||
object Loading : StatsScreenState()
|
||||
|
||||
@Immutable
|
||||
data class Success(
|
||||
val overview: StatsData.Overview,
|
||||
val titles: StatsData.Titles,
|
||||
val chapters: StatsData.Chapters,
|
||||
val trackers: StatsData.Trackers,
|
||||
) : StatsScreenState()
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
package eu.kanade.presentation.more.stats.components
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.RowScope
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.graphics.vector.rememberVectorPainter
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import eu.kanade.presentation.util.SecondaryItemAlpha
|
||||
import eu.kanade.presentation.util.padding
|
||||
|
||||
@Composable
|
||||
fun RowScope.StatsOverviewItem(
|
||||
title: String,
|
||||
subtitle: String,
|
||||
icon: ImageVector,
|
||||
) {
|
||||
BaseStatsItem(
|
||||
title = title,
|
||||
titleStyle = MaterialTheme.typography.titleLarge,
|
||||
subtitle = subtitle,
|
||||
subtitleStyle = MaterialTheme.typography.bodyMedium,
|
||||
icon = icon,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun RowScope.StatsItem(
|
||||
title: String,
|
||||
subtitle: String,
|
||||
) {
|
||||
BaseStatsItem(
|
||||
title = title,
|
||||
titleStyle = MaterialTheme.typography.bodyMedium,
|
||||
subtitle = subtitle,
|
||||
subtitleStyle = MaterialTheme.typography.labelSmall,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RowScope.BaseStatsItem(
|
||||
title: String,
|
||||
titleStyle: TextStyle,
|
||||
subtitle: String,
|
||||
subtitleStyle: TextStyle,
|
||||
icon: ImageVector? = null,
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.weight(1f),
|
||||
verticalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
Text(
|
||||
text = title,
|
||||
style = titleStyle
|
||||
.copy(fontWeight = FontWeight.Bold),
|
||||
textAlign = TextAlign.Center,
|
||||
maxLines = 1,
|
||||
)
|
||||
Text(
|
||||
text = subtitle,
|
||||
style = subtitleStyle
|
||||
.copy(
|
||||
color = MaterialTheme.colorScheme.onSurface
|
||||
.copy(alpha = SecondaryItemAlpha),
|
||||
),
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
if (icon != null) {
|
||||
Icon(
|
||||
painter = rememberVectorPainter(icon),
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package eu.kanade.presentation.more.stats.components
|
||||
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.ElevatedCard
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import eu.kanade.presentation.util.padding
|
||||
|
||||
@Composable
|
||||
fun StatsSection(
|
||||
@StringRes titleRes: Int,
|
||||
content: @Composable () -> Unit,
|
||||
) {
|
||||
Text(
|
||||
modifier = Modifier.padding(horizontal = MaterialTheme.padding.extraLarge),
|
||||
text = stringResource(titleRes),
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
)
|
||||
ElevatedCard(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(
|
||||
horizontal = MaterialTheme.padding.medium,
|
||||
vertical = MaterialTheme.padding.small,
|
||||
),
|
||||
shape = MaterialTheme.shapes.extraLarge,
|
||||
) {
|
||||
Column(modifier = Modifier.padding(MaterialTheme.padding.medium)) {
|
||||
content()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package eu.kanade.presentation.more.stats.data
|
||||
|
||||
sealed class StatsData {
|
||||
|
||||
data class Overview(
|
||||
val libraryMangaCount: Int,
|
||||
val completedMangaCount: Int,
|
||||
val totalReadDuration: Long,
|
||||
) : StatsData()
|
||||
|
||||
data class Titles(
|
||||
val globalUpdateItemCount: Int,
|
||||
val startedMangaCount: Int,
|
||||
val localMangaCount: Int,
|
||||
) : StatsData()
|
||||
|
||||
data class Chapters(
|
||||
val totalChapterCount: Int,
|
||||
val readChapterCount: Int,
|
||||
val downloadCount: Int,
|
||||
) : StatsData()
|
||||
|
||||
data class Trackers(
|
||||
val trackedTitleCount: Int,
|
||||
val meanScore: Double,
|
||||
val trackerCount: Int,
|
||||
) : StatsData()
|
||||
}
|
||||
Reference in New Issue
Block a user