mirror of
https://github.com/mihonapp/mihon.git
synced 2024-12-25 02:18:24 +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:
parent
e14909fff4
commit
3d7591feca
@ -44,7 +44,6 @@ fun <E> HashSet<E>.addOrRemove(value: E, shouldAdd: Boolean) {
|
|||||||
* access in an efficient way, and this method may actually be a lot slower. Only use for
|
* access in an efficient way, and this method may actually be a lot slower. Only use for
|
||||||
* collections that are created by code we control and are known to support random access.
|
* collections that are created by code we control and are known to support random access.
|
||||||
*/
|
*/
|
||||||
@Suppress("BanInlineOptIn")
|
|
||||||
@OptIn(ExperimentalContracts::class)
|
@OptIn(ExperimentalContracts::class)
|
||||||
inline fun <T> List<T>.fastFilter(predicate: (T) -> Boolean): List<T> {
|
inline fun <T> List<T>.fastFilter(predicate: (T) -> Boolean): List<T> {
|
||||||
contract { callsInPlace(predicate) }
|
contract { callsInPlace(predicate) }
|
||||||
@ -60,7 +59,6 @@ inline fun <T> List<T>.fastFilter(predicate: (T) -> Boolean): List<T> {
|
|||||||
* access in an efficient way, and this method may actually be a lot slower. Only use for
|
* access in an efficient way, and this method may actually be a lot slower. Only use for
|
||||||
* collections that are created by code we control and are known to support random access.
|
* collections that are created by code we control and are known to support random access.
|
||||||
*/
|
*/
|
||||||
@Suppress("BanInlineOptIn")
|
|
||||||
@OptIn(ExperimentalContracts::class)
|
@OptIn(ExperimentalContracts::class)
|
||||||
inline fun <T> List<T>.fastFilterNot(predicate: (T) -> Boolean): List<T> {
|
inline fun <T> List<T>.fastFilterNot(predicate: (T) -> Boolean): List<T> {
|
||||||
contract { callsInPlace(predicate) }
|
contract { callsInPlace(predicate) }
|
||||||
@ -77,7 +75,6 @@ inline fun <T> List<T>.fastFilterNot(predicate: (T) -> Boolean): List<T> {
|
|||||||
* access in an efficient way, and this method may actually be a lot slower. Only use for
|
* access in an efficient way, and this method may actually be a lot slower. Only use for
|
||||||
* collections that are created by code we control and are known to support random access.
|
* collections that are created by code we control and are known to support random access.
|
||||||
*/
|
*/
|
||||||
@Suppress("BanInlineOptIn")
|
|
||||||
@OptIn(ExperimentalContracts::class)
|
@OptIn(ExperimentalContracts::class)
|
||||||
inline fun <T, R> List<T>.fastMapNotNull(transform: (T) -> R?): List<R> {
|
inline fun <T, R> List<T>.fastMapNotNull(transform: (T) -> R?): List<R> {
|
||||||
contract { callsInPlace(transform) }
|
contract { callsInPlace(transform) }
|
||||||
@ -97,7 +94,6 @@ inline fun <T, R> List<T>.fastMapNotNull(transform: (T) -> R?): List<R> {
|
|||||||
* access in an efficient way, and this method may actually be a lot slower. Only use for
|
* access in an efficient way, and this method may actually be a lot slower. Only use for
|
||||||
* collections that are created by code we control and are known to support random access.
|
* collections that are created by code we control and are known to support random access.
|
||||||
*/
|
*/
|
||||||
@Suppress("BanInlineOptIn")
|
|
||||||
@OptIn(ExperimentalContracts::class)
|
@OptIn(ExperimentalContracts::class)
|
||||||
inline fun <T> List<T>.fastPartition(predicate: (T) -> Boolean): Pair<List<T>, List<T>> {
|
inline fun <T> List<T>.fastPartition(predicate: (T) -> Boolean): Pair<List<T>, List<T>> {
|
||||||
contract { callsInPlace(predicate) }
|
contract { callsInPlace(predicate) }
|
||||||
@ -112,3 +108,41 @@ inline fun <T> List<T>.fastPartition(predicate: (T) -> Boolean): Pair<List<T>, L
|
|||||||
}
|
}
|
||||||
return Pair(first, second)
|
return Pair(first, second)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the number of entries not matching the given [predicate].
|
||||||
|
*
|
||||||
|
* **Do not use for collections that come from public APIs**, since they may not support random
|
||||||
|
* access in an efficient way, and this method may actually be a lot slower. Only use for
|
||||||
|
* collections that are created by code we control and are known to support random access.
|
||||||
|
*/
|
||||||
|
@OptIn(ExperimentalContracts::class)
|
||||||
|
inline fun <T> List<T>.fastCountNot(predicate: (T) -> Boolean): Int {
|
||||||
|
contract { callsInPlace(predicate) }
|
||||||
|
var count = size
|
||||||
|
fastForEach { if (predicate(it)) --count }
|
||||||
|
return count
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a list containing only elements from the given collection
|
||||||
|
* having distinct keys returned by the given [selector] function.
|
||||||
|
*
|
||||||
|
* Among elements of the given collection with equal keys, only the first one will be present in the resulting list.
|
||||||
|
* The elements in the resulting list are in the same order as they were in the source collection.
|
||||||
|
*
|
||||||
|
* **Do not use for collections that come from public APIs**, since they may not support random
|
||||||
|
* access in an efficient way, and this method may actually be a lot slower. Only use for
|
||||||
|
* collections that are created by code we control and are known to support random access.
|
||||||
|
*/
|
||||||
|
@OptIn(ExperimentalContracts::class)
|
||||||
|
inline fun <T, K> List<T>.fastDistinctBy(selector: (T) -> K): List<T> {
|
||||||
|
contract { callsInPlace(selector) }
|
||||||
|
val set = HashSet<K>()
|
||||||
|
val list = ArrayList<T>()
|
||||||
|
fastForEach {
|
||||||
|
val key = selector(it)
|
||||||
|
if (set.add(key)) list.add(it)
|
||||||
|
}
|
||||||
|
return list
|
||||||
|
}
|
||||||
|
16
app/src/main/java/eu/kanade/core/util/DurationUtils.kt
Normal file
16
app/src/main/java/eu/kanade/core/util/DurationUtils.kt
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
package eu.kanade.core.util
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import eu.kanade.tachiyomi.R
|
||||||
|
import kotlin.time.Duration
|
||||||
|
|
||||||
|
fun Duration.toDurationString(context: Context, fallback: String): String {
|
||||||
|
return toComponents { days, hours, minutes, seconds, _ ->
|
||||||
|
buildList(4) {
|
||||||
|
if (days != 0L) add(context.getString(R.string.day_short, days))
|
||||||
|
if (hours != 0) add(context.getString(R.string.hour_short, hours))
|
||||||
|
if (minutes != 0 && (days == 0L || hours == 0)) add(context.getString(R.string.minute_short, minutes))
|
||||||
|
if (seconds != 0 && days == 0L && hours == 0) add(context.getString(R.string.seconds_short, seconds))
|
||||||
|
}.joinToString(" ").ifBlank { fallback }
|
||||||
|
}
|
||||||
|
}
|
@ -24,6 +24,10 @@ class HistoryRepositoryImpl(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override suspend fun getTotalReadDuration(): Long {
|
||||||
|
return handler.awaitOne { historyQueries.getReadDuration() }
|
||||||
|
}
|
||||||
|
|
||||||
override suspend fun resetHistory(historyId: Long) {
|
override suspend fun resetHistory(historyId: Long) {
|
||||||
try {
|
try {
|
||||||
handler.await { historyQueries.resetHistoryById(historyId) }
|
handler.await { historyQueries.resetHistoryById(historyId) }
|
||||||
|
@ -34,6 +34,7 @@ import eu.kanade.domain.extension.interactor.GetExtensionSources
|
|||||||
import eu.kanade.domain.extension.interactor.GetExtensionsByType
|
import eu.kanade.domain.extension.interactor.GetExtensionsByType
|
||||||
import eu.kanade.domain.history.interactor.GetHistory
|
import eu.kanade.domain.history.interactor.GetHistory
|
||||||
import eu.kanade.domain.history.interactor.GetNextChapters
|
import eu.kanade.domain.history.interactor.GetNextChapters
|
||||||
|
import eu.kanade.domain.history.interactor.GetTotalReadDuration
|
||||||
import eu.kanade.domain.history.interactor.RemoveHistory
|
import eu.kanade.domain.history.interactor.RemoveHistory
|
||||||
import eu.kanade.domain.history.interactor.UpsertHistory
|
import eu.kanade.domain.history.interactor.UpsertHistory
|
||||||
import eu.kanade.domain.history.repository.HistoryRepository
|
import eu.kanade.domain.history.repository.HistoryRepository
|
||||||
@ -120,6 +121,7 @@ class DomainModule : InjektModule {
|
|||||||
addFactory { GetHistory(get()) }
|
addFactory { GetHistory(get()) }
|
||||||
addFactory { UpsertHistory(get()) }
|
addFactory { UpsertHistory(get()) }
|
||||||
addFactory { RemoveHistory(get()) }
|
addFactory { RemoveHistory(get()) }
|
||||||
|
addFactory { GetTotalReadDuration(get()) }
|
||||||
|
|
||||||
addFactory { DeleteDownload(get(), get()) }
|
addFactory { DeleteDownload(get(), get()) }
|
||||||
|
|
||||||
|
@ -0,0 +1,12 @@
|
|||||||
|
package eu.kanade.domain.history.interactor
|
||||||
|
|
||||||
|
import eu.kanade.domain.history.repository.HistoryRepository
|
||||||
|
|
||||||
|
class GetTotalReadDuration(
|
||||||
|
private val repository: HistoryRepository,
|
||||||
|
) {
|
||||||
|
|
||||||
|
suspend fun await(): Long {
|
||||||
|
return repository.getTotalReadDuration()
|
||||||
|
}
|
||||||
|
}
|
@ -10,6 +10,8 @@ interface HistoryRepository {
|
|||||||
|
|
||||||
suspend fun getLastHistory(): HistoryWithRelations?
|
suspend fun getLastHistory(): HistoryWithRelations?
|
||||||
|
|
||||||
|
suspend fun getTotalReadDuration(): Long
|
||||||
|
|
||||||
suspend fun resetHistory(historyId: Long)
|
suspend fun resetHistory(historyId: Long)
|
||||||
|
|
||||||
suspend fun resetHistoryByMangaId(mangaId: Long)
|
suspend fun resetHistoryByMangaId(mangaId: Long)
|
||||||
|
@ -31,7 +31,7 @@ fun LanguageBadge(
|
|||||||
) {
|
) {
|
||||||
if (isLocal) {
|
if (isLocal) {
|
||||||
Badge(
|
Badge(
|
||||||
text = stringResource(R.string.local_source_badge),
|
text = stringResource(R.string.label_local),
|
||||||
color = MaterialTheme.colorScheme.tertiary,
|
color = MaterialTheme.colorScheme.tertiary,
|
||||||
textColor = MaterialTheme.colorScheme.onTertiary,
|
textColor = MaterialTheme.colorScheme.onTertiary,
|
||||||
)
|
)
|
||||||
|
@ -292,7 +292,7 @@ private fun FilterPage(
|
|||||||
.verticalScroll(rememberScrollState()),
|
.verticalScroll(rememberScrollState()),
|
||||||
) {
|
) {
|
||||||
FilterPageItem(
|
FilterPageItem(
|
||||||
label = stringResource(R.string.action_filter_downloaded),
|
label = stringResource(R.string.label_downloaded),
|
||||||
state = downloadFilter,
|
state = downloadFilter,
|
||||||
onClick = onDownloadFilterChanged,
|
onClick = onDownloadFilterChanged,
|
||||||
)
|
)
|
||||||
|
@ -270,7 +270,7 @@ private fun SearchResultItem(
|
|||||||
}
|
}
|
||||||
if (startDate.isNotBlank()) {
|
if (startDate.isNotBlank()) {
|
||||||
SearchResultItemDetails(
|
SearchResultItemDetails(
|
||||||
title = stringResource(R.string.track_start_date),
|
title = stringResource(R.string.label_started),
|
||||||
text = startDate,
|
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.HelpOutline
|
||||||
import androidx.compose.material.icons.outlined.Info
|
import androidx.compose.material.icons.outlined.Info
|
||||||
import androidx.compose.material.icons.outlined.Label
|
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.Settings
|
||||||
import androidx.compose.material.icons.outlined.SettingsBackupRestore
|
import androidx.compose.material.icons.outlined.SettingsBackupRestore
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
@ -41,6 +42,7 @@ fun MoreScreen(
|
|||||||
isFDroid: Boolean,
|
isFDroid: Boolean,
|
||||||
onClickDownloadQueue: () -> Unit,
|
onClickDownloadQueue: () -> Unit,
|
||||||
onClickCategories: () -> Unit,
|
onClickCategories: () -> Unit,
|
||||||
|
onClickStats: () -> Unit,
|
||||||
onClickBackupAndRestore: () -> Unit,
|
onClickBackupAndRestore: () -> Unit,
|
||||||
onClickSettings: () -> Unit,
|
onClickSettings: () -> Unit,
|
||||||
onClickAbout: () -> Unit,
|
onClickAbout: () -> Unit,
|
||||||
@ -132,6 +134,13 @@ fun MoreScreen(
|
|||||||
onPreferenceClick = onClickCategories,
|
onPreferenceClick = onClickCategories,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
item {
|
||||||
|
TextPreferenceWidget(
|
||||||
|
title = stringResource(R.string.label_stats),
|
||||||
|
icon = Icons.Outlined.QueryStats,
|
||||||
|
onPreferenceClick = onClickStats,
|
||||||
|
)
|
||||||
|
}
|
||||||
item {
|
item {
|
||||||
TextPreferenceWidget(
|
TextPreferenceWidget(
|
||||||
title = stringResource(R.string.label_backup),
|
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()
|
||||||
|
}
|
@ -107,6 +107,19 @@ class DownloadCache(
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the amount of downloaded chapters.
|
||||||
|
*/
|
||||||
|
fun getTotalDownloadCount(): Int {
|
||||||
|
renewCache()
|
||||||
|
|
||||||
|
return rootDownloadsDir.sourceDirs.values.sumOf { sourceDir ->
|
||||||
|
sourceDir.mangaDirs.values.sumOf { mangaDir ->
|
||||||
|
mangaDir.chapterDirs.size
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the amount of downloaded chapters for a manga.
|
* Returns the amount of downloaded chapters for a manga.
|
||||||
*
|
*
|
||||||
|
@ -205,6 +205,13 @@ class DownloadManager(
|
|||||||
.firstOrNull { it.chapter.id == chapter.id && it.chapter.manga_id == chapter.mangaId }
|
.firstOrNull { it.chapter.id == chapter.id && it.chapter.manga_id == chapter.mangaId }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the amount of downloaded chapters.
|
||||||
|
*/
|
||||||
|
fun getDownloadCount(): Int {
|
||||||
|
return cache.getTotalDownloadCount()
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the amount of downloaded chapters for a manga.
|
* Returns the amount of downloaded chapters for a manga.
|
||||||
*
|
*
|
||||||
|
@ -24,6 +24,7 @@ import okhttp3.OkHttpClient
|
|||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
import eu.kanade.domain.track.model.Track as DomainTrack
|
||||||
|
|
||||||
abstract class TrackService(val id: Long) {
|
abstract class TrackService(val id: Long) {
|
||||||
|
|
||||||
@ -59,6 +60,11 @@ abstract class TrackService(val id: Long) {
|
|||||||
|
|
||||||
abstract fun getScoreList(): List<String>
|
abstract fun getScoreList(): List<String>
|
||||||
|
|
||||||
|
// TODO: Store all scores as 10 point in the future maybe?
|
||||||
|
open fun get10PointScore(track: DomainTrack): Float {
|
||||||
|
return track.score
|
||||||
|
}
|
||||||
|
|
||||||
open fun indexToScore(index: Int): Float {
|
open fun indexToScore(index: Int): Float {
|
||||||
return index.toFloat()
|
return index.toFloat()
|
||||||
}
|
}
|
||||||
|
@ -11,6 +11,7 @@ import kotlinx.serialization.decodeFromString
|
|||||||
import kotlinx.serialization.encodeToString
|
import kotlinx.serialization.encodeToString
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
import eu.kanade.domain.track.model.Track as DomainTrack
|
||||||
|
|
||||||
class Anilist(private val context: Context, id: Long) : TrackService(id) {
|
class Anilist(private val context: Context, id: Long) : TrackService(id) {
|
||||||
|
|
||||||
@ -94,6 +95,11 @@ class Anilist(private val context: Context, id: Long) : TrackService(id) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun get10PointScore(track: DomainTrack): Float {
|
||||||
|
// Score is stored in 100 point format
|
||||||
|
return track.score / 10f
|
||||||
|
}
|
||||||
|
|
||||||
override fun indexToScore(index: Int): Float {
|
override fun indexToScore(index: Int): Float {
|
||||||
return when (scorePreference.get()) {
|
return when (scorePreference.get()) {
|
||||||
// 10 point
|
// 10 point
|
||||||
|
@ -91,9 +91,9 @@ class LibrarySettingsSheet(
|
|||||||
|
|
||||||
inner class FilterGroup : Group {
|
inner class FilterGroup : Group {
|
||||||
|
|
||||||
private val downloaded = Item.TriStateGroup(R.string.action_filter_downloaded, this)
|
private val downloaded = Item.TriStateGroup(R.string.label_downloaded, this)
|
||||||
private val unread = Item.TriStateGroup(R.string.action_filter_unread, this)
|
private val unread = Item.TriStateGroup(R.string.action_filter_unread, this)
|
||||||
private val started = Item.TriStateGroup(R.string.action_filter_started, this)
|
private val started = Item.TriStateGroup(R.string.label_started, this)
|
||||||
private val bookmarked = Item.TriStateGroup(R.string.action_filter_bookmarked, this)
|
private val bookmarked = Item.TriStateGroup(R.string.action_filter_bookmarked, this)
|
||||||
private val completed = Item.TriStateGroup(R.string.completed, this)
|
private val completed = Item.TriStateGroup(R.string.completed, this)
|
||||||
private val trackFilters: Map<Long, Item.TriStateGroup>
|
private val trackFilters: Map<Long, Item.TriStateGroup>
|
||||||
|
@ -20,6 +20,7 @@ import eu.kanade.tachiyomi.ui.base.controller.pushController
|
|||||||
import eu.kanade.tachiyomi.ui.category.CategoryController
|
import eu.kanade.tachiyomi.ui.category.CategoryController
|
||||||
import eu.kanade.tachiyomi.ui.download.DownloadController
|
import eu.kanade.tachiyomi.ui.download.DownloadController
|
||||||
import eu.kanade.tachiyomi.ui.setting.SettingsMainController
|
import eu.kanade.tachiyomi.ui.setting.SettingsMainController
|
||||||
|
import eu.kanade.tachiyomi.ui.stats.StatsController
|
||||||
import eu.kanade.tachiyomi.util.lang.launchIO
|
import eu.kanade.tachiyomi.util.lang.launchIO
|
||||||
import eu.kanade.tachiyomi.util.system.isInstalledFromFDroid
|
import eu.kanade.tachiyomi.util.system.isInstalledFromFDroid
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
@ -46,6 +47,7 @@ object MoreScreen : Screen {
|
|||||||
isFDroid = context.isInstalledFromFDroid(),
|
isFDroid = context.isInstalledFromFDroid(),
|
||||||
onClickDownloadQueue = { router.pushController(DownloadController()) },
|
onClickDownloadQueue = { router.pushController(DownloadController()) },
|
||||||
onClickCategories = { router.pushController(CategoryController()) },
|
onClickCategories = { router.pushController(CategoryController()) },
|
||||||
|
onClickStats = { router.pushController(StatsController()) },
|
||||||
onClickBackupAndRestore = { router.pushController(SettingsMainController.toBackupScreen()) },
|
onClickBackupAndRestore = { router.pushController(SettingsMainController.toBackupScreen()) },
|
||||||
onClickSettings = { router.pushController(SettingsMainController()) },
|
onClickSettings = { router.pushController(SettingsMainController()) },
|
||||||
onClickAbout = { router.pushController(SettingsMainController.toAboutScreen()) },
|
onClickAbout = { router.pushController(SettingsMainController.toAboutScreen()) },
|
||||||
|
@ -0,0 +1,13 @@
|
|||||||
|
package eu.kanade.tachiyomi.ui.stats
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import cafe.adriel.voyager.navigator.Navigator
|
||||||
|
import eu.kanade.tachiyomi.ui.base.controller.BasicFullComposeController
|
||||||
|
|
||||||
|
class StatsController : BasicFullComposeController() {
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
override fun ComposeContent() {
|
||||||
|
Navigator(screen = StatsScreen())
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,52 @@
|
|||||||
|
package eu.kanade.tachiyomi.ui.stats
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import cafe.adriel.voyager.core.model.rememberScreenModel
|
||||||
|
import cafe.adriel.voyager.core.screen.Screen
|
||||||
|
import cafe.adriel.voyager.core.screen.uniqueScreenKey
|
||||||
|
import cafe.adriel.voyager.navigator.currentOrThrow
|
||||||
|
import eu.kanade.presentation.components.AppBar
|
||||||
|
import eu.kanade.presentation.components.LoadingScreen
|
||||||
|
import eu.kanade.presentation.components.Scaffold
|
||||||
|
import eu.kanade.presentation.more.stats.StatsScreenContent
|
||||||
|
import eu.kanade.presentation.more.stats.StatsScreenState
|
||||||
|
import eu.kanade.presentation.util.LocalRouter
|
||||||
|
import eu.kanade.tachiyomi.R
|
||||||
|
|
||||||
|
class StatsScreen : Screen {
|
||||||
|
|
||||||
|
override val key = uniqueScreenKey
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
override fun Content() {
|
||||||
|
val router = LocalRouter.currentOrThrow
|
||||||
|
val context = LocalContext.current
|
||||||
|
|
||||||
|
val screenModel = rememberScreenModel { StatsScreenModel() }
|
||||||
|
val state by screenModel.state.collectAsState()
|
||||||
|
|
||||||
|
if (state is StatsScreenState.Loading) {
|
||||||
|
LoadingScreen()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
Scaffold(
|
||||||
|
topBar = { scrollBehavior ->
|
||||||
|
AppBar(
|
||||||
|
title = stringResource(R.string.label_stats),
|
||||||
|
navigateUp = router::popCurrentController,
|
||||||
|
scrollBehavior = scrollBehavior,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
) { paddingValues ->
|
||||||
|
StatsScreenContent(
|
||||||
|
state = state as StatsScreenState.Success,
|
||||||
|
paddingValues = paddingValues,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,152 @@
|
|||||||
|
package eu.kanade.tachiyomi.ui.stats
|
||||||
|
|
||||||
|
import cafe.adriel.voyager.core.model.StateScreenModel
|
||||||
|
import cafe.adriel.voyager.core.model.coroutineScope
|
||||||
|
import eu.kanade.core.util.fastCountNot
|
||||||
|
import eu.kanade.core.util.fastDistinctBy
|
||||||
|
import eu.kanade.core.util.fastFilter
|
||||||
|
import eu.kanade.core.util.fastFilterNot
|
||||||
|
import eu.kanade.core.util.fastMapNotNull
|
||||||
|
import eu.kanade.domain.history.interactor.GetTotalReadDuration
|
||||||
|
import eu.kanade.domain.library.model.LibraryManga
|
||||||
|
import eu.kanade.domain.library.service.LibraryPreferences
|
||||||
|
import eu.kanade.domain.manga.interactor.GetLibraryManga
|
||||||
|
import eu.kanade.domain.manga.model.isLocal
|
||||||
|
import eu.kanade.domain.track.interactor.GetTracks
|
||||||
|
import eu.kanade.domain.track.model.Track
|
||||||
|
import eu.kanade.presentation.more.stats.StatsScreenState
|
||||||
|
import eu.kanade.presentation.more.stats.data.StatsData
|
||||||
|
import eu.kanade.tachiyomi.data.download.DownloadManager
|
||||||
|
import eu.kanade.tachiyomi.data.preference.MANGA_HAS_UNREAD
|
||||||
|
import eu.kanade.tachiyomi.data.preference.MANGA_NON_COMPLETED
|
||||||
|
import eu.kanade.tachiyomi.data.preference.MANGA_NON_READ
|
||||||
|
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||||
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
|
import eu.kanade.tachiyomi.util.lang.launchIO
|
||||||
|
import kotlinx.coroutines.flow.update
|
||||||
|
import uy.kohesive.injekt.Injekt
|
||||||
|
import uy.kohesive.injekt.api.get
|
||||||
|
|
||||||
|
class StatsScreenModel(
|
||||||
|
private val downloadManager: DownloadManager = Injekt.get(),
|
||||||
|
private val getLibraryManga: GetLibraryManga = Injekt.get(),
|
||||||
|
private val getTotalReadDuration: GetTotalReadDuration = Injekt.get(),
|
||||||
|
private val getTracks: GetTracks = Injekt.get(),
|
||||||
|
private val preferences: LibraryPreferences = Injekt.get(),
|
||||||
|
private val trackManager: TrackManager = Injekt.get(),
|
||||||
|
) : StateScreenModel<StatsScreenState>(StatsScreenState.Loading) {
|
||||||
|
|
||||||
|
private val loggedServices by lazy { trackManager.services.fastFilter { it.isLogged } }
|
||||||
|
|
||||||
|
init {
|
||||||
|
coroutineScope.launchIO {
|
||||||
|
val libraryManga = getLibraryManga.await()
|
||||||
|
|
||||||
|
val distinctLibraryManga = libraryManga.fastDistinctBy { it.id }
|
||||||
|
|
||||||
|
val mangaTrackMap = getMangaTrackMap(distinctLibraryManga)
|
||||||
|
val scoredMangaTrackerMap = getScoredMangaTrackMap(mangaTrackMap)
|
||||||
|
|
||||||
|
val meanScore = getTrackMeanScore(scoredMangaTrackerMap)
|
||||||
|
|
||||||
|
val overviewStatData = StatsData.Overview(
|
||||||
|
libraryMangaCount = distinctLibraryManga.size,
|
||||||
|
completedMangaCount = distinctLibraryManga.count {
|
||||||
|
it.manga.status.toInt() == SManga.COMPLETED && it.unreadCount == 0L
|
||||||
|
},
|
||||||
|
totalReadDuration = getTotalReadDuration.await(),
|
||||||
|
)
|
||||||
|
|
||||||
|
val titlesStatData = StatsData.Titles(
|
||||||
|
globalUpdateItemCount = getGlobalUpdateItemCount(libraryManga),
|
||||||
|
startedMangaCount = distinctLibraryManga.count { it.hasStarted },
|
||||||
|
localMangaCount = distinctLibraryManga.count { it.manga.isLocal() },
|
||||||
|
)
|
||||||
|
|
||||||
|
val chaptersStatData = StatsData.Chapters(
|
||||||
|
totalChapterCount = distinctLibraryManga.sumOf { it.totalChapters }.toInt(),
|
||||||
|
readChapterCount = distinctLibraryManga.sumOf { it.readCount }.toInt(),
|
||||||
|
downloadCount = downloadManager.getDownloadCount(),
|
||||||
|
)
|
||||||
|
|
||||||
|
val trackersStatData = StatsData.Trackers(
|
||||||
|
trackedTitleCount = mangaTrackMap.count { it.value.isNotEmpty() },
|
||||||
|
meanScore = meanScore,
|
||||||
|
trackerCount = loggedServices.size,
|
||||||
|
)
|
||||||
|
|
||||||
|
mutableState.update {
|
||||||
|
StatsScreenState.Success(
|
||||||
|
overview = overviewStatData,
|
||||||
|
titles = titlesStatData,
|
||||||
|
chapters = chaptersStatData,
|
||||||
|
trackers = trackersStatData,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getGlobalUpdateItemCount(libraryManga: List<LibraryManga>): Int {
|
||||||
|
val includedCategories = preferences.libraryUpdateCategories().get().map { it.toLong() }
|
||||||
|
val includedManga = if (includedCategories.isNotEmpty()) {
|
||||||
|
libraryManga.filter { it.category in includedCategories }
|
||||||
|
} else {
|
||||||
|
libraryManga
|
||||||
|
}
|
||||||
|
|
||||||
|
val excludedCategories = preferences.libraryUpdateCategoriesExclude().get().map { it.toLong() }
|
||||||
|
val excludedMangaIds = if (excludedCategories.isNotEmpty()) {
|
||||||
|
libraryManga.fastMapNotNull { manga ->
|
||||||
|
manga.id.takeIf { manga.category in excludedCategories }
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
emptyList()
|
||||||
|
}
|
||||||
|
|
||||||
|
val updateRestrictions = preferences.libraryUpdateMangaRestriction().get()
|
||||||
|
return includedManga
|
||||||
|
.fastFilterNot { it.manga.id in excludedMangaIds }
|
||||||
|
.fastDistinctBy { it.manga.id }
|
||||||
|
.fastCountNot {
|
||||||
|
(MANGA_NON_COMPLETED in updateRestrictions && it.manga.status.toInt() == SManga.COMPLETED) ||
|
||||||
|
(MANGA_HAS_UNREAD in updateRestrictions && it.unreadCount != 0L) ||
|
||||||
|
(MANGA_NON_READ in updateRestrictions && it.totalChapters > 0 && !it.hasStarted)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun getMangaTrackMap(libraryManga: List<LibraryManga>): Map<Long, List<Track>> {
|
||||||
|
val loggedServicesIds = loggedServices.map { it.id }.toHashSet()
|
||||||
|
return libraryManga.associate { manga ->
|
||||||
|
val tracks = getTracks.await(manga.id)
|
||||||
|
.fastFilter { it.syncId in loggedServicesIds }
|
||||||
|
|
||||||
|
manga.id to tracks
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getScoredMangaTrackMap(mangaTrackMap: Map<Long, List<Track>>): Map<Long, List<Track>> {
|
||||||
|
return mangaTrackMap.mapNotNull { (mangaId, tracks) ->
|
||||||
|
val trackList = tracks.mapNotNull { track ->
|
||||||
|
track.takeIf { it.score > 0.0 }
|
||||||
|
}
|
||||||
|
if (trackList.isEmpty()) return@mapNotNull null
|
||||||
|
mangaId to trackList
|
||||||
|
}.toMap()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getTrackMeanScore(scoredMangaTrackMap: Map<Long, List<Track>>): Double {
|
||||||
|
return scoredMangaTrackMap
|
||||||
|
.map { (_, tracks) ->
|
||||||
|
tracks.map {
|
||||||
|
get10PointScore(it)
|
||||||
|
}.average()
|
||||||
|
}
|
||||||
|
.fastFilter { !it.isNaN() }
|
||||||
|
.average()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun get10PointScore(track: Track): Float {
|
||||||
|
val service = trackManager.getService(track.syncId)!!
|
||||||
|
return service.get10PointScore(track)
|
||||||
|
}
|
||||||
|
}
|
@ -67,3 +67,7 @@ SET
|
|||||||
last_read = :readAt,
|
last_read = :readAt,
|
||||||
time_read = time_read + :time_read
|
time_read = time_read + :time_read
|
||||||
WHERE chapter_id = :chapterId;
|
WHERE chapter_id = :chapterId;
|
||||||
|
|
||||||
|
getReadDuration:
|
||||||
|
SELECT coalesce(sum(time_read), 0)
|
||||||
|
FROM history;
|
||||||
|
@ -23,6 +23,7 @@
|
|||||||
<string name="label_recent_manga">History</string>
|
<string name="label_recent_manga">History</string>
|
||||||
<string name="label_sources">Sources</string>
|
<string name="label_sources">Sources</string>
|
||||||
<string name="label_backup">Backup and restore</string>
|
<string name="label_backup">Backup and restore</string>
|
||||||
|
<string name="label_stats">Statistics</string>
|
||||||
<string name="label_migration">Migrate</string>
|
<string name="label_migration">Migrate</string>
|
||||||
<string name="label_extensions">Extensions</string>
|
<string name="label_extensions">Extensions</string>
|
||||||
<string name="label_extension_info">Extension info</string>
|
<string name="label_extension_info">Extension info</string>
|
||||||
@ -30,6 +31,11 @@
|
|||||||
<string name="label_default">Default</string>
|
<string name="label_default">Default</string>
|
||||||
<string name="label_warning">Warning</string>
|
<string name="label_warning">Warning</string>
|
||||||
|
|
||||||
|
<!-- Shared labels -->
|
||||||
|
<string name="label_started">Started</string>
|
||||||
|
<string name="label_local">Local</string>
|
||||||
|
<string name="label_downloaded">Downloaded</string>
|
||||||
|
|
||||||
<string name="unlock_app">Unlock Tachiyomi</string>
|
<string name="unlock_app">Unlock Tachiyomi</string>
|
||||||
<string name="confirm_lock_change">Authenticate to confirm change</string>
|
<string name="confirm_lock_change">Authenticate to confirm change</string>
|
||||||
<string name="confirm_exit">Press back again to exit</string>
|
<string name="confirm_exit">Press back again to exit</string>
|
||||||
@ -38,11 +44,9 @@
|
|||||||
<string name="action_settings">Settings</string>
|
<string name="action_settings">Settings</string>
|
||||||
<string name="action_menu">Menu</string>
|
<string name="action_menu">Menu</string>
|
||||||
<string name="action_filter">Filter</string>
|
<string name="action_filter">Filter</string>
|
||||||
<string name="action_filter_downloaded">Downloaded</string>
|
|
||||||
<string name="action_filter_bookmarked">Bookmarked</string>
|
<string name="action_filter_bookmarked">Bookmarked</string>
|
||||||
<string name="action_filter_tracked">Tracked</string>
|
<string name="action_filter_tracked">Tracked</string>
|
||||||
<string name="action_filter_unread">Unread</string>
|
<string name="action_filter_unread">Unread</string>
|
||||||
<string name="action_filter_started">Started</string>
|
|
||||||
<!-- reserved for #4048 -->
|
<!-- reserved for #4048 -->
|
||||||
<string name="action_filter_empty">Remove filter</string>
|
<string name="action_filter_empty">Remove filter</string>
|
||||||
<string name="action_sort_alpha">Alphabetically</string>
|
<string name="action_sort_alpha">Alphabetically</string>
|
||||||
@ -576,7 +580,6 @@
|
|||||||
|
|
||||||
<!-- Library fragment -->
|
<!-- Library fragment -->
|
||||||
<string name="updating_category">Updating category</string>
|
<string name="updating_category">Updating category</string>
|
||||||
<string name="local_source_badge">Local</string>
|
|
||||||
<string name="manga_from_library">From library</string>
|
<string name="manga_from_library">From library</string>
|
||||||
<string name="downloaded_chapters">Downloaded chapters</string>
|
<string name="downloaded_chapters">Downloaded chapters</string>
|
||||||
<string name="badges_header">Badges</string>
|
<string name="badges_header">Badges</string>
|
||||||
@ -699,7 +702,6 @@
|
|||||||
<string name="title">Title</string>
|
<string name="title">Title</string>
|
||||||
<string name="status">Status</string>
|
<string name="status">Status</string>
|
||||||
<string name="track_status">Status</string>
|
<string name="track_status">Status</string>
|
||||||
<string name="track_start_date">Started</string>
|
|
||||||
<string name="track_started_reading_date">Start date</string>
|
<string name="track_started_reading_date">Start date</string>
|
||||||
<string name="track_finished_reading_date">Finish date</string>
|
<string name="track_finished_reading_date">Finish date</string>
|
||||||
<string name="track_type">Type</string>
|
<string name="track_type">Type</string>
|
||||||
@ -783,6 +785,24 @@
|
|||||||
<string name="crash_screen_description">%s ran into an unexpected error. We suggest you screenshot this message, dump the crash logs, and then share it in our support channel on Discord.</string>
|
<string name="crash_screen_description">%s ran into an unexpected error. We suggest you screenshot this message, dump the crash logs, and then share it in our support channel on Discord.</string>
|
||||||
<string name="crash_screen_restart_application">Restart the application</string>
|
<string name="crash_screen_restart_application">Restart the application</string>
|
||||||
|
|
||||||
|
<!-- Stats screen -->
|
||||||
|
<string name="label_overview_section">Overview</string>
|
||||||
|
<string name="label_completed_titles">Completed entries</string>
|
||||||
|
<string name="label_read_duration">Read duration</string>
|
||||||
|
<string name="label_titles_section">Entries</string>
|
||||||
|
<string name="label_titles_in_global_update">In global update</string>
|
||||||
|
<string name="label_total_chapters">Total</string>
|
||||||
|
<string name="label_read_chapters">Read</string>
|
||||||
|
<string name="label_tracker_section">Trackers</string>
|
||||||
|
<string name="label_tracked_titles">Tracked entries</string>
|
||||||
|
<string name="label_mean_score">Mean score</string>
|
||||||
|
<string name="label_used">Used</string>
|
||||||
|
<string name="not_applicable">N/A</string>
|
||||||
|
<string name="day_short">%dd</string>
|
||||||
|
<string name="hour_short">%dh</string>
|
||||||
|
<string name="minute_short">%dm</string>
|
||||||
|
<string name="seconds_short">%ds</string>
|
||||||
|
|
||||||
<!-- Downloads activity and service -->
|
<!-- Downloads activity and service -->
|
||||||
<string name="download_queue_error">Couldn\'t download chapters. You can try again in the downloads section</string>
|
<string name="download_queue_error">Couldn\'t download chapters. You can try again in the downloads section</string>
|
||||||
<string name="download_insufficient_space">Couldn\'t download chapters due to low storage space</string>
|
<string name="download_insufficient_space">Couldn\'t download chapters due to low storage space</string>
|
||||||
|
Loading…
Reference in New Issue
Block a user