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:
AntsyLich
2022-11-27 02:50:26 +06:00
committed by GitHub
parent e14909fff4
commit 3d7591feca
26 changed files with 695 additions and 14 deletions

View File

@@ -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,
)

View File

@@ -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,
)

View File

@@ -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,
)
}

View File

@@ -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),

View File

@@ -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),
)
}
}
}

View File

@@ -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()
}

View File

@@ -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,
)
}
}
}

View File

@@ -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()
}
}
}

View File

@@ -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()
}