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:
@@ -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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user