Cleanup Library presenter (#8284)

* yeet observable + minor cleanup

* move [getTracksFlow] to domain

* Lint

* Review changes

Co-Authored-By: Andreas <6576096+ghostbear@users.noreply.github.com>

* Review Changes 2

* Stuff

* Rename + Rebase

* Lint

Co-authored-by: Andreas <6576096+ghostbear@users.noreply.github.com>
This commit is contained in:
AntsyLich 2022-10-28 21:44:05 +06:00 committed by GitHub
parent 37b7efbc87
commit e36d31bf0f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 73 additions and 115 deletions

View file

@ -63,6 +63,7 @@ import eu.kanade.domain.source.repository.SourceDataRepository
import eu.kanade.domain.source.repository.SourceRepository import eu.kanade.domain.source.repository.SourceRepository
import eu.kanade.domain.track.interactor.DeleteTrack import eu.kanade.domain.track.interactor.DeleteTrack
import eu.kanade.domain.track.interactor.GetTracks import eu.kanade.domain.track.interactor.GetTracks
import eu.kanade.domain.track.interactor.GetTracksPerManga
import eu.kanade.domain.track.interactor.InsertTrack import eu.kanade.domain.track.interactor.InsertTrack
import eu.kanade.domain.track.repository.TrackRepository import eu.kanade.domain.track.repository.TrackRepository
import eu.kanade.domain.updates.interactor.GetUpdates import eu.kanade.domain.updates.interactor.GetUpdates
@ -104,6 +105,7 @@ class DomainModule : InjektModule {
addSingletonFactory<TrackRepository> { TrackRepositoryImpl(get()) } addSingletonFactory<TrackRepository> { TrackRepositoryImpl(get()) }
addFactory { DeleteTrack(get()) } addFactory { DeleteTrack(get()) }
addFactory { GetTracksPerManga(get()) }
addFactory { GetTracks(get()) } addFactory { GetTracks(get()) }
addFactory { InsertTrack(get()) } addFactory { InsertTrack(get()) }

View file

@ -19,10 +19,6 @@ class GetTracks(
} }
} }
fun subscribe(): Flow<List<Track>> {
return trackRepository.getTracksAsFlow()
}
fun subscribe(mangaId: Long): Flow<List<Track>> { fun subscribe(mangaId: Long): Flow<List<Track>> {
return trackRepository.getTracksByMangaIdAsFlow(mangaId) return trackRepository.getTracksByMangaIdAsFlow(mangaId)
} }

View file

@ -0,0 +1,20 @@
package eu.kanade.domain.track.interactor
import eu.kanade.domain.track.repository.TrackRepository
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
class GetTracksPerManga(
private val trackRepository: TrackRepository,
) {
fun subscribe(): Flow<Map<Long, List<Long>>> {
return trackRepository.getTracksAsFlow().map { tracks ->
tracks
.groupBy { it.mangaId }
.mapValues { entry ->
entry.value.map { it.syncId }
}
}
}
}

View file

@ -25,6 +25,7 @@ import eu.kanade.tachiyomi.ui.category.CategoryController
import eu.kanade.tachiyomi.ui.main.MainActivity import eu.kanade.tachiyomi.ui.main.MainActivity
import eu.kanade.tachiyomi.ui.manga.MangaController import eu.kanade.tachiyomi.ui.manga.MangaController
import eu.kanade.tachiyomi.util.lang.launchIO import eu.kanade.tachiyomi.util.lang.launchIO
import eu.kanade.tachiyomi.util.lang.launchUI
import eu.kanade.tachiyomi.util.system.toast import eu.kanade.tachiyomi.util.system.toast
import kotlinx.coroutines.cancel import kotlinx.coroutines.cancel
@ -126,7 +127,6 @@ class LibraryController(
settingsSheet = LibrarySettingsSheet(router) { group -> settingsSheet = LibrarySettingsSheet(router) { group ->
when (group) { when (group) {
is LibrarySettingsSheet.Filter.FilterGroup -> onFilterChanged() is LibrarySettingsSheet.Filter.FilterGroup -> onFilterChanged()
is LibrarySettingsSheet.Sort.SortGroup -> onSortChanged()
else -> {} // Handled via different mechanisms else -> {} // Handled via different mechanisms
} }
} }
@ -152,12 +152,10 @@ class LibraryController(
} }
private fun onFilterChanged() { private fun onFilterChanged() {
presenter.requestFilterUpdate() viewScope.launchUI {
activity?.invalidateOptionsMenu() presenter.requestFilterUpdate()
} activity?.invalidateOptionsMenu()
}
private fun onSortChanged() {
presenter.requestSortUpdate()
} }
fun search(query: String) { fun search(query: String) {
@ -180,7 +178,7 @@ class LibraryController(
* Clear all of the manga currently selected, and * Clear all of the manga currently selected, and
* invalidate the action mode to revert the top toolbar * invalidate the action mode to revert the top toolbar
*/ */
fun clearSelection() { private fun clearSelection() {
presenter.clearSelection() presenter.clearSelection()
} }

View file

@ -11,11 +11,8 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.util.fastAny import androidx.compose.ui.util.fastAny
import androidx.compose.ui.util.fastMap import androidx.compose.ui.util.fastMap
import com.jakewharton.rxrelay.BehaviorRelay
import eu.kanade.core.prefs.CheckboxState import eu.kanade.core.prefs.CheckboxState
import eu.kanade.core.prefs.PreferenceMutableState import eu.kanade.core.prefs.PreferenceMutableState
import eu.kanade.core.util.asFlow
import eu.kanade.core.util.asObservable
import eu.kanade.domain.base.BasePreferences import eu.kanade.domain.base.BasePreferences
import eu.kanade.domain.category.interactor.GetCategories import eu.kanade.domain.category.interactor.GetCategories
import eu.kanade.domain.category.interactor.SetMangaCategories import eu.kanade.domain.category.interactor.SetMangaCategories
@ -32,7 +29,7 @@ import eu.kanade.domain.manga.interactor.UpdateManga
import eu.kanade.domain.manga.model.Manga import eu.kanade.domain.manga.model.Manga
import eu.kanade.domain.manga.model.MangaUpdate import eu.kanade.domain.manga.model.MangaUpdate
import eu.kanade.domain.manga.model.isLocal import eu.kanade.domain.manga.model.isLocal
import eu.kanade.domain.track.interactor.GetTracks import eu.kanade.domain.track.interactor.GetTracksPerManga
import eu.kanade.presentation.category.visualName import eu.kanade.presentation.category.visualName
import eu.kanade.presentation.library.LibraryState import eu.kanade.presentation.library.LibraryState
import eu.kanade.presentation.library.LibraryStateImpl import eu.kanade.presentation.library.LibraryStateImpl
@ -49,16 +46,16 @@ import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
import eu.kanade.tachiyomi.util.lang.launchIO import eu.kanade.tachiyomi.util.lang.launchIO
import eu.kanade.tachiyomi.util.lang.launchNonCancellable import eu.kanade.tachiyomi.util.lang.launchNonCancellable
import eu.kanade.tachiyomi.util.lang.withIOContext
import eu.kanade.tachiyomi.util.removeCovers import eu.kanade.tachiyomi.util.removeCovers
import eu.kanade.tachiyomi.widget.ExtendedNavigationView.Item.TriStateGroup.State import eu.kanade.tachiyomi.widget.ExtendedNavigationView.Item.TriStateGroup.State
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onStart
import rx.Observable import kotlinx.coroutines.flow.receiveAsFlow
import rx.android.schedulers.AndroidSchedulers
import rx.schedulers.Schedulers
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import java.text.Collator import java.text.Collator
@ -79,7 +76,7 @@ typealias LibraryMap = Map<Long, List<LibraryItem>>
class LibraryPresenter( class LibraryPresenter(
private val state: LibraryStateImpl = LibraryState() as LibraryStateImpl, private val state: LibraryStateImpl = LibraryState() as LibraryStateImpl,
private val getLibraryManga: GetLibraryManga = Injekt.get(), private val getLibraryManga: GetLibraryManga = Injekt.get(),
private val getTracks: GetTracks = Injekt.get(), private val getTracksPerManga: GetTracksPerManga = Injekt.get(),
private val getCategories: GetCategories = Injekt.get(), private val getCategories: GetCategories = Injekt.get(),
private val getChapterByMangaId: GetChapterByMangaId = Injekt.get(), private val getChapterByMangaId: GetChapterByMangaId = Injekt.get(),
private val setReadStatus: SetReadStatus = Injekt.get(), private val setReadStatus: SetReadStatus = Injekt.get(),
@ -111,15 +108,8 @@ class LibraryPresenter(
val isDownloadOnly: Boolean by preferences.downloadedOnly().asState() val isDownloadOnly: Boolean by preferences.downloadedOnly().asState()
val isIncognitoMode: Boolean by preferences.incognitoMode().asState() val isIncognitoMode: Boolean by preferences.incognitoMode().asState()
/** private val _filterChanges: Channel<Unit> = Channel(Int.MAX_VALUE)
* Relay used to apply the UI filters to the last emission of the library. private val filterChanges = _filterChanges.receiveAsFlow().onStart { emit(Unit) }
*/
private val filterTriggerRelay = BehaviorRelay.create(Unit)
/**
* Relay used to apply the selected sorting method to the last emission of the library.
*/
private val sortTriggerRelay = BehaviorRelay.create(Unit)
private var librarySubscription: Job? = null private var librarySubscription: Job? = null
@ -141,18 +131,14 @@ class LibraryPresenter(
*/ */
if (librarySubscription == null || librarySubscription!!.isCancelled) { if (librarySubscription == null || librarySubscription!!.isCancelled) {
librarySubscription = presenterScope.launchIO { librarySubscription = presenterScope.launchIO {
getLibraryFlow().asObservable() combine(getLibraryFlow(), getTracksPerManga.subscribe(), filterChanges) { library, tracks, _ ->
.combineLatest(getFilterObservable()) { lib, tracks -> library.mangaMap
lib.copy(mangaMap = applyFilters(lib.mangaMap, tracks)) .applyFilters(tracks)
} .applySort(library.categories)
.combineLatest(sortTriggerRelay.observeOn(Schedulers.io())) { lib, _ -> }
lib.copy(mangaMap = applySort(lib.categories, lib.mangaMap))
}
.observeOn(AndroidSchedulers.mainThread())
.asFlow()
.collectLatest { .collectLatest {
state.isLoading = false state.isLoading = false
loadedManga = it.mangaMap loadedManga = it
} }
} }
} }
@ -160,21 +146,24 @@ class LibraryPresenter(
/** /**
* Applies library filters to the given map of manga. * Applies library filters to the given map of manga.
*
* @param map the map to filter.
*/ */
private fun applyFilters(map: LibraryMap, trackMap: Map<Long, Map<Long, Boolean>>): LibraryMap { private fun LibraryMap.applyFilters(trackMap: Map<Long, List<Long>>): LibraryMap {
val downloadedOnly = preferences.downloadedOnly().get() val downloadedOnly = preferences.downloadedOnly().get()
val filterDownloaded = libraryPreferences.filterDownloaded().get() val filterDownloaded = libraryPreferences.filterDownloaded().get()
val filterUnread = libraryPreferences.filterUnread().get() val filterUnread = libraryPreferences.filterUnread().get()
val filterStarted = libraryPreferences.filterStarted().get() val filterStarted = libraryPreferences.filterStarted().get()
val filterBookmarked = libraryPreferences.filterBookmarked().get() val filterBookmarked = libraryPreferences.filterBookmarked().get()
val filterCompleted = libraryPreferences.filterCompleted().get() val filterCompleted = libraryPreferences.filterCompleted().get()
val loggedInServices = trackManager.services.filter { trackService -> trackService.isLogged }
val loggedInTrackServices = trackManager.services.filter { trackService -> trackService.isLogged }
.associate { trackService -> .associate { trackService ->
Pair(trackService.id, libraryPreferences.filterTracking(trackService.id.toInt()).get()) trackService.id to libraryPreferences.filterTracking(trackService.id.toInt()).get()
} }
val isNotAnyLoggedIn = !loggedInServices.values.any() val isNotLoggedInAnyTrack = loggedInTrackServices.isEmpty()
val excludedTracks = loggedInTrackServices.mapNotNull { if (it.value == State.EXCLUDE.value) it.key else null }
val includedTracks = loggedInTrackServices.mapNotNull { if (it.value == State.INCLUDE.value) it.key else null }
val trackFiltersIsIgnored = includedTracks.isEmpty() && excludedTracks.isEmpty()
val filterFnDownloaded: (LibraryItem) -> Boolean = downloaded@{ item -> val filterFnDownloaded: (LibraryItem) -> Boolean = downloaded@{ item ->
if (!downloadedOnly && filterDownloaded == State.IGNORE.value) return@downloaded true if (!downloadedOnly && filterDownloaded == State.IGNORE.value) return@downloaded true
@ -237,25 +226,21 @@ class LibraryPresenter(
} }
val filterFnTracking: (LibraryItem) -> Boolean = tracking@{ item -> val filterFnTracking: (LibraryItem) -> Boolean = tracking@{ item ->
if (isNotAnyLoggedIn) return@tracking true if (isNotLoggedInAnyTrack || trackFiltersIsIgnored) return@tracking true
val trackedManga = trackMap[item.libraryManga.manga.id] val mangaTracks = trackMap[item.libraryManga.id].orEmpty()
val containsExclude = loggedInServices.filterValues { it == State.EXCLUDE.value } val exclude = mangaTracks.filter { it in excludedTracks }
val containsInclude = loggedInServices.filterValues { it == State.INCLUDE.value } val include = mangaTracks.filter { it in includedTracks }
if (!containsExclude.any() && !containsInclude.any()) return@tracking true // TODO: Simplify the filter logic
if (includedTracks.isNotEmpty() && excludedTracks.isNotEmpty()) {
val exclude = trackedManga?.filterKeys { containsExclude.containsKey(it) }?.values ?: emptyList() return@tracking if (exclude.isNotEmpty()) false else include.isNotEmpty()
val include = trackedManga?.filterKeys { containsInclude.containsKey(it) }?.values ?: emptyList()
if (containsInclude.any() && containsExclude.any()) {
return@tracking if (exclude.isNotEmpty()) !exclude.any() else include.any()
} }
if (containsExclude.any()) return@tracking !exclude.any() if (excludedTracks.isNotEmpty()) return@tracking exclude.isEmpty()
if (containsInclude.any()) return@tracking include.any() if (includedTracks.isNotEmpty()) return@tracking include.isNotEmpty()
return@tracking false return@tracking false
} }
@ -271,15 +256,13 @@ class LibraryPresenter(
) )
} }
return map.mapValues { entry -> entry.value.filter(filterFn) } return this.mapValues { entry -> entry.value.filter(filterFn) }
} }
/** /**
* Applies library sorting to the given map of manga. * Applies library sorting to the given map of manga.
*
* @param map the map to sort.
*/ */
private fun applySort(categories: List<Category>, map: LibraryMap): LibraryMap { private fun LibraryMap.applySort(categories: List<Category>): LibraryMap {
val sortModes = categories.associate { it.id to it.sort } val sortModes = categories.associate { it.id to it.sort }
val locale = Locale.getDefault() val locale = Locale.getDefault()
@ -321,7 +304,7 @@ class LibraryPresenter(
} }
} }
return map.mapValues { entry -> return this.mapValues { entry ->
val comparator = if (sortModes[entry.key]!!.isAscending) { val comparator = if (sortModes[entry.key]!!.isAscending) {
Comparator(sortFn) Comparator(sortFn)
} else { } else {
@ -373,48 +356,11 @@ class LibraryPresenter(
} }
} }
/**
* Get the tracked manga from the database and checks if the filter gets changed
*
* @return an observable of tracked manga.
*/
private fun getFilterObservable(): Observable<Map<Long, Map<Long, Boolean>>> {
return filterTriggerRelay.observeOn(Schedulers.io())
.combineLatest(getTracksFlow().asObservable().observeOn(Schedulers.io())) { _, tracks -> tracks }
}
/**
* Get the tracked manga from the database
*
* @return an observable of tracked manga.
*/
private fun getTracksFlow(): Flow<Map<Long, Map<Long, Boolean>>> {
// TODO: Move this to domain/data layer
return getTracks.subscribe()
.map { tracks ->
tracks
.groupBy { it.mangaId }
.mapValues { tracksForMangaId ->
// Check if any of the trackers is logged in for the current manga id
tracksForMangaId.value.associate {
Pair(it.syncId, trackManager.getService(it.syncId)?.isLogged ?: false)
}
}
}
}
/** /**
* Requests the library to be filtered. * Requests the library to be filtered.
*/ */
fun requestFilterUpdate() { suspend fun requestFilterUpdate() = withIOContext {
filterTriggerRelay.call(Unit) _filterChanges.send(Unit)
}
/**
* Requests the library to be sorted.
*/
fun requestSortUpdate() {
sortTriggerRelay.call(Unit)
} }
/** /**
@ -432,9 +378,9 @@ class LibraryPresenter(
*/ */
suspend fun getCommonCategories(mangas: List<Manga>): Collection<Category> { suspend fun getCommonCategories(mangas: List<Manga>): Collection<Category> {
if (mangas.isEmpty()) return emptyList() if (mangas.isEmpty()) return emptyList()
return mangas.toSet() return mangas
.map { getCategories.await(it.id) } .map { getCategories.await(it.id).toSet() }
.reduce { set1, set2 -> set1.intersect(set2).toMutableList() } .reduce { set1, set2 -> set1.intersect(set2) }
} }
/** /**
@ -444,9 +390,9 @@ class LibraryPresenter(
*/ */
suspend fun getMixCategories(mangas: List<Manga>): Collection<Category> { suspend fun getMixCategories(mangas: List<Manga>): Collection<Category> {
if (mangas.isEmpty()) return emptyList() if (mangas.isEmpty()) return emptyList()
val mangaCategories = mangas.toSet().map { getCategories.await(it.id) } val mangaCategories = mangas.map { getCategories.await(it.id).toSet() }
val common = mangaCategories.reduce { set1, set2 -> set1.intersect(set2).toMutableList() } val common = mangaCategories.reduce { set1, set2 -> set1.intersect(set2) }
return mangaCategories.flatten().distinct().subtract(common).toMutableList() return mangaCategories.flatten().distinct().subtract(common)
} }
/** /**
@ -524,10 +470,10 @@ class LibraryPresenter(
*/ */
fun setMangaCategories(mangaList: List<Manga>, addCategories: List<Long>, removeCategories: List<Long>) { fun setMangaCategories(mangaList: List<Manga>, addCategories: List<Long>, removeCategories: List<Long>) {
presenterScope.launchNonCancellable { presenterScope.launchNonCancellable {
mangaList.map { manga -> mangaList.forEach { manga ->
val categoryIds = getCategories.await(manga.id) val categoryIds = getCategories.await(manga.id)
.map { it.id } .map { it.id }
.subtract(removeCategories) .subtract(removeCategories.toSet())
.plus(addCategories) .plus(addCategories)
.toList() .toList()
@ -649,10 +595,6 @@ class LibraryPresenter(
} }
} }
private fun <T, U, R> Observable<T>.combineLatest(o2: Observable<U>, combineFn: (T, U) -> R): Observable<R> {
return Observable.combineLatest(this, o2, combineFn)
}
sealed class Dialog { sealed class Dialog {
data class ChangeCategory(val manga: List<Manga>, val initialSelection: List<CheckboxState<Category>>) : Dialog() data class ChangeCategory(val manga: List<Manga>, val initialSelection: List<CheckboxState<Category>>) : Dialog()
data class DeleteManga(val manga: List<Manga>) : Dialog() data class DeleteManga(val manga: List<Manga>) : Dialog()