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

@@ -107,6 +107,19 @@ class DownloadCache(
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.
*

View File

@@ -205,6 +205,13 @@ class DownloadManager(
.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.
*

View File

@@ -24,6 +24,7 @@ import okhttp3.OkHttpClient
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
import eu.kanade.domain.track.model.Track as DomainTrack
abstract class TrackService(val id: Long) {
@@ -59,6 +60,11 @@ abstract class TrackService(val id: Long) {
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 {
return index.toFloat()
}

View File

@@ -11,6 +11,7 @@ import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import uy.kohesive.injekt.injectLazy
import eu.kanade.domain.track.model.Track as DomainTrack
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 {
return when (scorePreference.get()) {
// 10 point

View File

@@ -91,9 +91,9 @@ class LibrarySettingsSheet(
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 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 completed = Item.TriStateGroup(R.string.completed, this)
private val trackFilters: Map<Long, Item.TriStateGroup>

View File

@@ -20,6 +20,7 @@ import eu.kanade.tachiyomi.ui.base.controller.pushController
import eu.kanade.tachiyomi.ui.category.CategoryController
import eu.kanade.tachiyomi.ui.download.DownloadController
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.system.isInstalledFromFDroid
import kotlinx.coroutines.flow.MutableStateFlow
@@ -46,6 +47,7 @@ object MoreScreen : Screen {
isFDroid = context.isInstalledFromFDroid(),
onClickDownloadQueue = { router.pushController(DownloadController()) },
onClickCategories = { router.pushController(CategoryController()) },
onClickStats = { router.pushController(StatsController()) },
onClickBackupAndRestore = { router.pushController(SettingsMainController.toBackupScreen()) },
onClickSettings = { router.pushController(SettingsMainController()) },
onClickAbout = { router.pushController(SettingsMainController.toAboutScreen()) },

View File

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

View File

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

View File

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