From 37207ed58bf4aa63ab0ffaf6d571f37eac5c152b Mon Sep 17 00:00:00 2001 From: AntsyLich <59261191+AntsyLich@users.noreply.github.com> Date: Fri, 28 Oct 2022 21:44:05 +0600 Subject: [PATCH] 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> (cherry picked from commit e36d31bf0fff9652652319fa8b4fc700edc1442a) # Conflicts: # app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryController.kt # app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryPresenter.kt --- .../java/eu/kanade/domain/DomainModule.kt | 2 + .../domain/track/interactor/GetTracks.kt | 4 - .../track/interactor/GetTracksPerManga.kt | 26 +++ .../tachiyomi/ui/library/LibraryController.kt | 22 +-- .../tachiyomi/ui/library/LibraryPresenter.kt | 180 ++++++------------ 5 files changed, 99 insertions(+), 135 deletions(-) create mode 100644 app/src/main/java/eu/kanade/domain/track/interactor/GetTracksPerManga.kt diff --git a/app/src/main/java/eu/kanade/domain/DomainModule.kt b/app/src/main/java/eu/kanade/domain/DomainModule.kt index d1a141b32..cf50e363b 100644 --- a/app/src/main/java/eu/kanade/domain/DomainModule.kt +++ b/app/src/main/java/eu/kanade/domain/DomainModule.kt @@ -63,6 +63,7 @@ import eu.kanade.domain.source.repository.SourceDataRepository import eu.kanade.domain.source.repository.SourceRepository import eu.kanade.domain.track.interactor.DeleteTrack 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.repository.TrackRepository import eu.kanade.domain.updates.interactor.GetUpdates @@ -104,6 +105,7 @@ class DomainModule : InjektModule { addSingletonFactory { TrackRepositoryImpl(get()) } addFactory { DeleteTrack(get()) } + addFactory { GetTracksPerManga(get()) } addFactory { GetTracks(get()) } addFactory { InsertTrack(get()) } diff --git a/app/src/main/java/eu/kanade/domain/track/interactor/GetTracks.kt b/app/src/main/java/eu/kanade/domain/track/interactor/GetTracks.kt index 324aed24d..9d699d1a6 100644 --- a/app/src/main/java/eu/kanade/domain/track/interactor/GetTracks.kt +++ b/app/src/main/java/eu/kanade/domain/track/interactor/GetTracks.kt @@ -40,10 +40,6 @@ class GetTracks( } } - fun subscribe(): Flow> { - return trackRepository.getTracksAsFlow() - } - fun subscribe(mangaId: Long): Flow> { return trackRepository.getTracksByMangaIdAsFlow(mangaId) } diff --git a/app/src/main/java/eu/kanade/domain/track/interactor/GetTracksPerManga.kt b/app/src/main/java/eu/kanade/domain/track/interactor/GetTracksPerManga.kt new file mode 100644 index 000000000..730f31d22 --- /dev/null +++ b/app/src/main/java/eu/kanade/domain/track/interactor/GetTracksPerManga.kt @@ -0,0 +1,26 @@ +package eu.kanade.domain.track.interactor + +import eu.kanade.domain.track.repository.TrackRepository +import eu.kanade.tachiyomi.data.track.TrackManager +import exh.md.utils.FollowStatus +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map + +class GetTracksPerManga( + private val trackRepository: TrackRepository, +) { + + fun subscribe(): Flow>> { + return trackRepository.getTracksAsFlow().map { tracks -> + tracks + .groupBy { it.mangaId } + .mapValues { entry -> + entry.value + // SY --> + .filterNot { it.syncId == TrackManager.MDLIST && it.status == FollowStatus.UNFOLLOWED.int.toLong() } + // SY <-- + .map { it.syncId } + } + } + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryController.kt index 0cfdf5202..d108893db 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryController.kt @@ -32,6 +32,7 @@ import eu.kanade.tachiyomi.ui.main.MainActivity import eu.kanade.tachiyomi.ui.manga.MangaController import eu.kanade.tachiyomi.ui.reader.ReaderActivity import eu.kanade.tachiyomi.util.lang.launchIO +import eu.kanade.tachiyomi.util.lang.launchUI import eu.kanade.tachiyomi.util.system.toast import exh.favorites.FavoritesIntroDialog import exh.favorites.FavoritesSyncStatus @@ -209,9 +210,8 @@ class LibraryController( settingsSheet = LibrarySettingsSheet(router) { group -> when (group) { is LibrarySettingsSheet.Filter.FilterGroup -> onFilterChanged() - is LibrarySettingsSheet.Sort.SortGroup -> onSortChanged() // SY --> - is LibrarySettingsSheet.Grouping.InternalGroup -> onGroupSettingChanged() + is LibrarySettingsSheet.Grouping.InternalGroup -> onGroupChanged() // SY <-- else -> {} // Handled via different mechanisms } @@ -238,20 +238,20 @@ class LibraryController( } private fun onFilterChanged() { - presenter.requestFilterUpdate() - activity?.invalidateOptionsMenu() + viewScope.launchUI { + presenter.requestFilterUpdate() + activity?.invalidateOptionsMenu() + } } // SY --> - private fun onGroupSettingChanged() { - presenter.requestGroupsUpdate() + private fun onGroupChanged() { + viewScope.launchUI { + presenter.requestGroupUpdate() + } } // SY <-- - private fun onSortChanged() { - presenter.requestSortUpdate() - } - fun search(query: String) { presenter.searchQuery = query } @@ -272,7 +272,7 @@ class LibraryController( * Clear all of the manga currently selected, and * invalidate the action mode to revert the top toolbar */ - fun clearSelection() { + private fun clearSelection() { presenter.clearSelection() } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryPresenter.kt index 26ca843bc..cf2a12070 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryPresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryPresenter.kt @@ -13,11 +13,8 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.util.fastAny import androidx.compose.ui.util.fastMap -import com.jakewharton.rxrelay.BehaviorRelay import eu.kanade.core.prefs.CheckboxState import eu.kanade.core.prefs.PreferenceMutableState -import eu.kanade.core.util.asFlow -import eu.kanade.core.util.asObservable import eu.kanade.domain.UnsortedPreferences import eu.kanade.domain.base.BasePreferences import eu.kanade.domain.category.interactor.GetCategories @@ -46,6 +43,7 @@ import eu.kanade.domain.manga.model.MangaUpdate import eu.kanade.domain.manga.model.isLocal import eu.kanade.domain.source.service.SourcePreferences import eu.kanade.domain.track.interactor.GetTracks +import eu.kanade.domain.track.interactor.GetTracksPerManga import eu.kanade.domain.track.model.Track import eu.kanade.presentation.category.visualName import eu.kanade.presentation.library.LibraryState @@ -89,6 +87,7 @@ import exh.util.isLewd import exh.util.nullIfBlank import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job +import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.asFlow import kotlinx.coroutines.flow.collectLatest @@ -96,12 +95,10 @@ import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onStart +import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.flow.toList import kotlinx.coroutines.runBlocking -import rx.Observable -import rx.android.schedulers.AndroidSchedulers -import rx.schedulers.Schedulers import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get import java.text.Collator @@ -122,7 +119,7 @@ typealias LibraryMap = Map> class LibraryPresenter( private val state: LibraryStateImpl = LibraryState() as LibraryStateImpl, 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 getChapterByMangaId: GetChapterByMangaId = Injekt.get(), private val setReadStatus: SetReadStatus = Injekt.get(), @@ -139,6 +136,7 @@ class LibraryPresenter( private val unsortedPreferences: UnsortedPreferences = Injekt.get(), private val sourcePreferences: SourcePreferences = Injekt.get(), private val searchEngine: SearchEngine = SearchEngine(), + private val getTracks: GetTracks = Injekt.get(), private val customMangaManager: CustomMangaManager = Injekt.get(), private val getMergedMangaById: GetMergedMangaById = Injekt.get(), private val getMergedChaptersByMangaId: GetMergedChapterByMangaId = Injekt.get(), @@ -169,15 +167,13 @@ class LibraryPresenter( val isDownloadOnly: Boolean by preferences.downloadedOnly().asState() val isIncognitoMode: Boolean by preferences.incognitoMode().asState() - /** - * Relay used to apply the UI filters to the last emission of the library. - */ - private val filterTriggerRelay = BehaviorRelay.create(Unit) + private val _filterChanges: Channel = Channel(Int.MAX_VALUE) + private val filterChanges = _filterChanges.receiveAsFlow().onStart { emit(Unit) } - /** - * Relay used to apply the selected sorting method to the last emission of the library. - */ - private val sortTriggerRelay = BehaviorRelay.create(Unit) + // SY --> + private val _groupChanges: Channel = Channel(Int.MAX_VALUE) + private val groupChanges = _groupChanges.receiveAsFlow().onStart { emit(Unit) } + // SY <-- private var librarySubscription: Job? = null @@ -191,11 +187,6 @@ class LibraryPresenter( service.id to preferences.context.getString(service.nameRes()) } } - - /** - * Relay used to apply the UI update to the last emission of the library. - */ - private val groupingTriggerRelay = BehaviorRelay.create(Unit) // SY <-- override fun onCreate(savedState: Bundle?) { @@ -226,28 +217,21 @@ class LibraryPresenter( */ if (librarySubscription == null || librarySubscription!!.isCancelled) { librarySubscription = presenterScope.launchIO { - getLibraryFlow().asObservable() - // SY --> - .combineLatest(groupingTriggerRelay.observeOn(Schedulers.io())) { lib, _ -> - val (map, categories) = applyGrouping(lib.mangaMap, lib.categories) - lib.copy(mangaMap = map, categories = categories) - } + combine(getLibraryFlow(), getTracksPerManga.subscribe(), filterChanges /* SY --> */, groupChanges/* SY <-- */) { library, tracks, _, _ -> + library.mangaMap + .applyFilters(tracks) + .applySort(library.categories) + // SY --> + .applyGrouping(library.categories) // SY <-- - .combineLatest(getFilterObservable()) { lib, tracks -> - lib.copy(mangaMap = applyFilters(lib.mangaMap, tracks)) - } - .combineLatest(sortTriggerRelay.observeOn(Schedulers.io())) { lib, _ -> - lib.copy(mangaMap = applySort(lib.categories, lib.mangaMap)) - } - .observeOn(AndroidSchedulers.mainThread()) - .asFlow() + } .collectLatest { // SY --> state.groupType = libraryPreferences.groupLibraryBy().get() state.categories = it.categories // SY <-- state.isLoading = false - loadedManga = it.mangaMap + loadedManga = /* SY --> */ it.mangaMap /* SY <-- */ } } } @@ -255,21 +239,25 @@ class LibraryPresenter( /** * Applies library filters to the given map of manga. - * - * @param map the map to filter. */ - private fun applyFilters(map: LibraryMap, trackMap: Map>): LibraryMap { + private fun LibraryMap.applyFilters(trackMap: Map>): LibraryMap { val downloadedOnly = preferences.downloadedOnly().get() val filterDownloaded = libraryPreferences.filterDownloaded().get() val filterUnread = libraryPreferences.filterUnread().get() val filterStarted = libraryPreferences.filterStarted().get() val filterBookmarked = libraryPreferences.filterBookmarked().get() val filterCompleted = libraryPreferences.filterCompleted().get() - val loggedInServices = trackManager.services.filter { trackService -> trackService.isLogged } + + val loggedInTrackServices = trackManager.services.filter { trackService -> trackService.isLogged } .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() + // SY --> val filterLewd = libraryPreferences.filterLewd().get() // SY <-- @@ -335,25 +323,21 @@ class LibraryPresenter( } 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 containsInclude = loggedInServices.filterValues { it == State.INCLUDE.value } + val exclude = mangaTracks.filter { it in excludedTracks } + val include = mangaTracks.filter { it in includedTracks } - if (!containsExclude.any() && !containsInclude.any()) return@tracking true - - val exclude = trackedManga?.filter { containsExclude.containsKey(it.key) && it.value }?.values ?: emptyList() - val include = trackedManga?.filter { containsInclude.containsKey(it.key) && it.value }?.values ?: emptyList() - - if (containsInclude.any() && containsExclude.any()) { - return@tracking if (exclude.isNotEmpty()) !exclude.any() else include.any() + // TODO: Simplify the filter logic + if (includedTracks.isNotEmpty() && excludedTracks.isNotEmpty()) { + return@tracking if (exclude.isNotEmpty()) false else include.isNotEmpty() } - 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 } @@ -385,15 +369,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. - * - * @param map the map to sort. */ - private fun applySort(categories: List, map: LibraryMap): LibraryMap { + private fun LibraryMap.applySort(categories: List): LibraryMap { // SY --> val listOfTags by lazy { libraryPreferences.sortTagsForLibrary().get() @@ -469,7 +451,7 @@ class LibraryPresenter( } } - return map.mapValues { entry -> + return this.mapValues { entry -> // SY --> val isAscending = if (groupType == LibraryGroup.BY_DEFAULT) { sortModes[entry.key]!!.isAscending @@ -539,85 +521,47 @@ class LibraryPresenter( } // SY --> - private fun applyGrouping(map: LibraryMap, categories: List): Pair> { + private fun LibraryMap.applyGrouping(categories: List): Library { val groupType = libraryPreferences.groupLibraryBy().get() var editedCategories = categories val items = when (groupType) { - LibraryGroup.BY_DEFAULT -> map + LibraryGroup.BY_DEFAULT -> this LibraryGroup.UNGROUPED -> { editedCategories = listOf(Category(0, "All", 0, 0)) mapOf( - 0L to map.values.flatten().distinctBy { it.libraryManga.manga.id }, + 0L to this.values.flatten().distinctBy { it.libraryManga.manga.id }, ) } else -> { val (items, customCategories) = getGroupedMangaItems( groupType = groupType, - libraryManga = map.values.flatten().distinctBy { it.libraryManga.manga.id }, + libraryManga = this.values.flatten().distinctBy { it.libraryManga.manga.id }, ) editedCategories = customCategories items } } - return items to editedCategories + return Library(editedCategories, items) } // SY <-- - /** - * Get the tracked manga from the database and checks if the filter gets changed - * - * @return an observable of tracked manga. - */ - private fun getFilterObservable(): Observable>> { - 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>> { - // 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.takeUnless { isLogged -> isLogged == true && it.syncId == TrackManager.MDLIST && it.status == FollowStatus.UNFOLLOWED.int.toLong() } ?: false) - } - } - } - } - /** * Requests the library to be filtered. */ - fun requestFilterUpdate() { - filterTriggerRelay.call(Unit) + suspend fun requestFilterUpdate() = withIOContext { + _filterChanges.send(Unit) } // SY --> /** - * Requests the library to have groups refreshed. + * Requests the library to be grouped. */ - fun requestGroupsUpdate() { - groupingTriggerRelay.call(Unit) + suspend fun requestGroupUpdate() = withIOContext { + _groupChanges.send(Unit) } - // SY <-- - /** - * Requests the library to be sorted. - */ - fun requestSortUpdate() { - sortTriggerRelay.call(Unit) - } - /** * Called when a manga is opened. */ @@ -633,9 +577,9 @@ class LibraryPresenter( */ suspend fun getCommonCategories(mangas: List): Collection { if (mangas.isEmpty()) return emptyList() - return mangas.toSet() - .map { getCategories.await(it.id) } - .reduce { set1, set2 -> set1.intersect(set2).toMutableList() } + return mangas + .map { getCategories.await(it.id).toSet() } + .reduce { set1, set2 -> set1.intersect(set2) } } /** @@ -645,9 +589,9 @@ class LibraryPresenter( */ suspend fun getMixCategories(mangas: List): Collection { if (mangas.isEmpty()) return emptyList() - val mangaCategories = mangas.toSet().map { getCategories.await(it.id) } - val common = mangaCategories.reduce { set1, set2 -> set1.intersect(set2).toMutableList() } - return mangaCategories.flatten().distinct().subtract(common).toMutableList() + val mangaCategories = mangas.map { getCategories.await(it.id).toSet() } + val common = mangaCategories.reduce { set1, set2 -> set1.intersect(set2) } + return mangaCategories.flatten().distinct().subtract(common) } /** @@ -789,10 +733,10 @@ class LibraryPresenter( */ fun setMangaCategories(mangaList: List, addCategories: List, removeCategories: List) { presenterScope.launchNonCancellable { - mangaList.map { manga -> + mangaList.forEach { manga -> val categoryIds = getCategories.await(manga.id) .map { it.id } - .subtract(removeCategories) + .subtract(removeCategories.toSet()) .plus(addCategories) .toList() @@ -1101,10 +1045,6 @@ class LibraryPresenter( } } - private fun Observable.combineLatest(o2: Observable, combineFn: (T, U) -> R): Observable { - return Observable.combineLatest(this, o2, combineFn) - } - sealed class Dialog { data class ChangeCategory(val manga: List, val initialSelection: List>) : Dialog() data class DeleteManga(val manga: List) : Dialog()