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