From 8a74faa4f034c49e2ac5346be8ce1c768fd78859 Mon Sep 17 00:00:00 2001 From: Koitharu Date: Thu, 5 Sep 2024 09:24:13 +0300 Subject: [PATCH] Fix Downloaded quick filter (close #1076, close #1079) --- .../koitharu/kotatsu/core/util/ext/Flow.kt | 3 + .../explore/data/MangaSourcesRepository.kt | 3 +- .../favourites/domain/FavouritesRepository.kt | 7 +++ .../domain/LocalFavoritesObserver.kt | 42 ++++++++++++++ .../ui/list/FavouritesListViewModel.kt | 48 ++++++---------- .../history/data/HistoryLocalObserver.kt | 35 ++++++++++++ .../kotatsu/history/data/HistoryRepository.kt | 4 ++ .../history/ui/HistoryListViewModel.kt | 41 ++++---------- .../local/domain/LocalObserveMapper.kt | 55 +++++++++++++++++++ 9 files changed, 175 insertions(+), 63 deletions(-) create mode 100644 app/src/main/kotlin/org/koitharu/kotatsu/favourites/domain/LocalFavoritesObserver.kt create mode 100644 app/src/main/kotlin/org/koitharu/kotatsu/history/data/HistoryLocalObserver.kt create mode 100644 app/src/main/kotlin/org/koitharu/kotatsu/local/domain/LocalObserveMapper.kt diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/Flow.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/Flow.kt index 5cb93035d..2ebd4ce7f 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/Flow.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/Flow.kt @@ -9,6 +9,7 @@ import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.firstOrNull +import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onCompletion @@ -129,3 +130,5 @@ fun combine( suspend fun Flow.firstNotNull(): T = checkNotNull(first { x -> x != null }) suspend fun Flow.firstNotNullOrNull(): T? = firstOrNull { x -> x != null } + +fun Flow>.flattenLatest() = flatMapLatest { it } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/explore/data/MangaSourcesRepository.kt b/app/src/main/kotlin/org/koitharu/kotatsu/explore/data/MangaSourcesRepository.kt index 5965308b8..2ffbd8d9c 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/explore/data/MangaSourcesRepository.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/explore/data/MangaSourcesRepository.kt @@ -27,6 +27,7 @@ import org.koitharu.kotatsu.core.parser.external.ExternalMangaSource import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.observeAsFlow import org.koitharu.kotatsu.core.ui.util.ReversibleHandle +import org.koitharu.kotatsu.core.util.ext.flattenLatest import org.koitharu.kotatsu.parsers.model.ContentType import org.koitharu.kotatsu.parsers.model.MangaParserSource import org.koitharu.kotatsu.parsers.model.MangaSource @@ -168,7 +169,7 @@ class MangaSourcesRepository @Inject constructor( dao.observeEnabled(order).map { it.toSources(skipNsfw, order) } - }.flatMapLatest { it } + }.flattenLatest() .onStart { assimilateNewSources() } .combine(observeExternalSources()) { enabled, external -> val list = ArrayList(enabled.size + external.size) diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/favourites/domain/FavouritesRepository.kt b/app/src/main/kotlin/org/koitharu/kotatsu/favourites/domain/FavouritesRepository.kt index fa28d9288..85f654492 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/favourites/domain/FavouritesRepository.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/favourites/domain/FavouritesRepository.kt @@ -27,6 +27,7 @@ import javax.inject.Inject @Reusable class FavouritesRepository @Inject constructor( private val db: MangaDatabase, + private val localObserver: LocalFavoritesObserver, ) { suspend fun getAllManga(): List { @@ -40,6 +41,9 @@ class FavouritesRepository @Inject constructor( } fun observeAll(order: ListSortOrder, filterOptions: Set, limit: Int): Flow> { + if (ListFilterOption.Downloaded in filterOptions) { + return localObserver.observeAll(order, filterOptions - ListFilterOption.Downloaded, limit) + } return db.getFavouritesDao().observeAll(order, filterOptions, limit) .mapItems { it.toManga() } } @@ -55,6 +59,9 @@ class FavouritesRepository @Inject constructor( filterOptions: Set, limit: Int ): Flow> { + if (ListFilterOption.Downloaded in filterOptions) { + return localObserver.observeAll(categoryId, order, filterOptions - ListFilterOption.Downloaded, limit) + } return db.getFavouritesDao().observeAll(categoryId, order, filterOptions, limit) .mapItems { it.toManga() } } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/favourites/domain/LocalFavoritesObserver.kt b/app/src/main/kotlin/org/koitharu/kotatsu/favourites/domain/LocalFavoritesObserver.kt new file mode 100644 index 000000000..0bee8280c --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/favourites/domain/LocalFavoritesObserver.kt @@ -0,0 +1,42 @@ +package org.koitharu.kotatsu.favourites.domain + +import dagger.Reusable +import kotlinx.coroutines.flow.Flow +import org.koitharu.kotatsu.core.db.MangaDatabase +import org.koitharu.kotatsu.core.db.entity.toManga +import org.koitharu.kotatsu.core.db.entity.toMangaTags +import org.koitharu.kotatsu.favourites.data.FavouriteManga +import org.koitharu.kotatsu.list.domain.ListFilterOption +import org.koitharu.kotatsu.list.domain.ListSortOrder +import org.koitharu.kotatsu.local.data.LocalMangaRepository +import org.koitharu.kotatsu.local.domain.LocalObserveMapper +import org.koitharu.kotatsu.parsers.model.Manga +import javax.inject.Inject + +@Reusable +class LocalFavoritesObserver @Inject constructor( + localMangaRepository: LocalMangaRepository, + private val db: MangaDatabase, +) : LocalObserveMapper(localMangaRepository, limitStep = 10) { + + fun observeAll( + order: ListSortOrder, + filterOptions: Set, + limit: Int + ): Flow> = observe(limit) { newLimit -> + db.getFavouritesDao().observeAll(order, filterOptions, newLimit) + } + + fun observeAll( + categoryId: Long, + order: ListSortOrder, + filterOptions: Set, + limit: Int + ): Flow> = observe(limit) { newLimit -> + db.getFavouritesDao().observeAll(categoryId, order, filterOptions, newLimit) + } + + override fun toManga(e: FavouriteManga) = e.manga.toManga(e.tags.toMangaTags()) + + override fun toResult(e: FavouriteManga, manga: Manga) = manga +} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/favourites/ui/list/FavouritesListViewModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/favourites/ui/list/FavouritesListViewModel.kt index a5ead0d80..3e784a114 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/favourites/ui/list/FavouritesListViewModel.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/favourites/ui/list/FavouritesListViewModel.kt @@ -4,26 +4,22 @@ import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.async -import kotlinx.coroutines.awaitAll -import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.filterNotNull -import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.plus import org.koitharu.kotatsu.R -import org.koitharu.kotatsu.core.model.isLocal import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.ListMode import org.koitharu.kotatsu.core.prefs.observeAsFlow import org.koitharu.kotatsu.core.ui.util.ReversibleAction import org.koitharu.kotatsu.core.util.ext.call +import org.koitharu.kotatsu.core.util.ext.flattenLatest import org.koitharu.kotatsu.download.ui.worker.DownloadWorker import org.koitharu.kotatsu.favourites.domain.FavoritesListQuickFilter import org.koitharu.kotatsu.favourites.domain.FavouritesRepository @@ -39,12 +35,11 @@ import org.koitharu.kotatsu.list.ui.model.EmptyState import org.koitharu.kotatsu.list.ui.model.ListModel import org.koitharu.kotatsu.list.ui.model.LoadingState import org.koitharu.kotatsu.list.ui.model.toErrorState -import org.koitharu.kotatsu.local.data.LocalMangaRepository import org.koitharu.kotatsu.parsers.model.Manga import java.util.concurrent.atomic.AtomicBoolean import javax.inject.Inject -private const val PAGE_SIZE = 20 +private const val PAGE_SIZE = 16 @HiltViewModel class FavouritesListViewModel @Inject constructor( @@ -53,7 +48,6 @@ class FavouritesListViewModel @Inject constructor( private val mangaListMapper: MangaListMapper, private val markAsReadUseCase: MarkAsReadUseCase, private val quickFilter: FavoritesListQuickFilter, - private val localMangaRepository: LocalMangaRepository, settings: AppSettings, downloadScheduler: DownloadWorker.Scheduler, ) : MangaListViewModel(settings, downloadScheduler), QuickFilterListener by quickFilter { @@ -130,30 +124,32 @@ class FavouritesListViewModel @Inject constructor( } private suspend fun List.mapList(mode: ListMode, filters: Set): List { - val list = if (ListFilterOption.Downloaded in filters) { - mapToLocal() - } else { - this - } - if (list.isEmpty()) { + if (isEmpty()) { return if (filters.isEmpty()) { listOf(getEmptyState(hasFilters = false)) } else { listOfNotNull(quickFilter.filterItem(filters), getEmptyState(hasFilters = true)) } } - val result = ArrayList(list.size + 1) + val result = ArrayList(size + 1) quickFilter.filterItem(filters)?.let(result::add) - mangaListMapper.toListModelList(result, list, mode) + mangaListMapper.toListModelList(result, this, mode) return result } private fun observeFavorites() = if (categoryId == NO_ID) { - combine(sortOrder.filterNotNull(), quickFilter.appliedOptions.combineWithSettings(), limit, ::Triple) - .flatMapLatest { repository.observeAll(it.first, it.second - ListFilterOption.Downloaded, it.third) } + combine( + sortOrder.filterNotNull(), + quickFilter.appliedOptions.combineWithSettings(), + limit, + ) { order, filters, limit -> + isReady.set(false) + repository.observeAll(order, filters, limit) + }.flattenLatest() } else { - combine(quickFilter.appliedOptions, limit, ::Pair) - .flatMapLatest { repository.observeAll(categoryId, it.first - ListFilterOption.Downloaded, it.second) } + combine(quickFilter.appliedOptions.combineWithSettings(), limit) { filters, limit -> + repository.observeAll(categoryId, filters, limit) + }.flattenLatest() } private fun getEmptyState(hasFilters: Boolean) = if (hasFilters) { @@ -175,16 +171,4 @@ class FavouritesListViewModel @Inject constructor( actionStringRes = 0, ) } - - private suspend fun List.mapToLocal(): List = coroutineScope { - map { - async { - if (it.isLocal) { - it - } else { - localMangaRepository.findSavedManga(it)?.manga - } - } - }.awaitAll().filterNotNull() - } } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/history/data/HistoryLocalObserver.kt b/app/src/main/kotlin/org/koitharu/kotatsu/history/data/HistoryLocalObserver.kt new file mode 100644 index 000000000..e1324a2a3 --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/history/data/HistoryLocalObserver.kt @@ -0,0 +1,35 @@ +package org.koitharu.kotatsu.history.data + +import dagger.Reusable +import org.koitharu.kotatsu.core.db.MangaDatabase +import org.koitharu.kotatsu.core.db.entity.toManga +import org.koitharu.kotatsu.core.db.entity.toMangaTags +import org.koitharu.kotatsu.history.domain.model.MangaWithHistory +import org.koitharu.kotatsu.list.domain.ListFilterOption +import org.koitharu.kotatsu.list.domain.ListSortOrder +import org.koitharu.kotatsu.local.data.LocalMangaRepository +import org.koitharu.kotatsu.local.domain.LocalObserveMapper +import org.koitharu.kotatsu.parsers.model.Manga +import javax.inject.Inject + +@Reusable +class HistoryLocalObserver @Inject constructor( + localMangaRepository: LocalMangaRepository, + private val db: MangaDatabase, +) : LocalObserveMapper(localMangaRepository, limitStep = 10) { + + fun observeAll( + order: ListSortOrder, + filterOptions: Set, + limit: Int + ) = observe(limit) { newLimit -> + db.getHistoryDao().observeAll(order, filterOptions, newLimit) + } + + override fun toManga(e: HistoryWithManga) = e.manga.toManga(e.tags.toMangaTags()) + + override fun toResult(e: HistoryWithManga, manga: Manga) = MangaWithHistory( + manga = manga, + history = e.history.toMangaHistory(), + ) +} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/history/data/HistoryRepository.kt b/app/src/main/kotlin/org/koitharu/kotatsu/history/data/HistoryRepository.kt index 73808775a..03ccd91e5 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/history/data/HistoryRepository.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/history/data/HistoryRepository.kt @@ -39,6 +39,7 @@ class HistoryRepository @Inject constructor( private val settings: AppSettings, private val scrobblers: Set<@JvmSuppressWildcards Scrobbler>, private val mangaRepository: MangaDataRepository, + private val localObserver: HistoryLocalObserver, private val newChaptersUseCaseProvider: Provider, ) { @@ -80,6 +81,9 @@ class HistoryRepository @Inject constructor( filterOptions: Set, limit: Int ): Flow> { + if (ListFilterOption.Downloaded in filterOptions) { + return localObserver.observeAll(order, filterOptions - ListFilterOption.Downloaded, limit) + } return db.getHistoryDao().observeAll(order, filterOptions, limit).mapItems { MangaWithHistory( it.manga.toManga(it.tags.toMangaTags()), diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/history/ui/HistoryListViewModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/history/ui/HistoryListViewModel.kt index 414d1df73..aa099e398 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/history/ui/HistoryListViewModel.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/history/ui/HistoryListViewModel.kt @@ -3,21 +3,16 @@ package org.koitharu.kotatsu.history.ui import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.async -import kotlinx.coroutines.awaitAll -import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.plus import org.koitharu.kotatsu.R import org.koitharu.kotatsu.core.model.MangaHistory -import org.koitharu.kotatsu.core.model.isLocal import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.ListMode import org.koitharu.kotatsu.core.prefs.observeAsFlow @@ -25,6 +20,7 @@ import org.koitharu.kotatsu.core.prefs.observeAsStateFlow import org.koitharu.kotatsu.core.ui.util.ReversibleAction import org.koitharu.kotatsu.core.util.ext.calculateTimeAgo import org.koitharu.kotatsu.core.util.ext.call +import org.koitharu.kotatsu.core.util.ext.flattenLatest import org.koitharu.kotatsu.core.util.ext.onFirst import org.koitharu.kotatsu.download.ui.worker.DownloadWorker import org.koitharu.kotatsu.history.data.HistoryRepository @@ -42,20 +38,18 @@ import org.koitharu.kotatsu.list.ui.model.ListHeader import org.koitharu.kotatsu.list.ui.model.ListModel import org.koitharu.kotatsu.list.ui.model.LoadingState import org.koitharu.kotatsu.list.ui.model.toErrorState -import org.koitharu.kotatsu.local.data.LocalMangaRepository import org.koitharu.kotatsu.parsers.model.Manga import java.time.Instant import java.util.concurrent.atomic.AtomicBoolean import javax.inject.Inject -private const val PAGE_SIZE = 20 +private const val PAGE_SIZE = 16 @HiltViewModel class HistoryListViewModel @Inject constructor( private val repository: HistoryRepository, settings: AppSettings, private val mangaListMapper: MangaListMapper, - private val localMangaRepository: LocalMangaRepository, private val markAsReadUseCase: MarkAsReadUseCase, private val quickFilter: HistoryListQuickFilter, downloadScheduler: DownloadWorker.Scheduler, @@ -144,21 +138,22 @@ class HistoryListViewModel @Inject constructor( } } - private fun observeHistory() = combine(sortOrder, quickFilter.appliedOptions.combineWithSettings(), limit, ::Triple) - .flatMapLatest { repository.observeAllWithHistory(it.first, it.second - ListFilterOption.Downloaded, it.third) } + private fun observeHistory() = combine( + sortOrder, + quickFilter.appliedOptions.combineWithSettings(), + limit, + ) { order, filters, limit -> + isReady.set(false) + repository.observeAllWithHistory(order, filters, limit) + }.flattenLatest() private suspend fun mapList( - historyList: List, + list: List, grouped: Boolean, mode: ListMode, filters: Set, isIncognito: Boolean, ): List { - val list = if (ListFilterOption.Downloaded in filters) { - historyList.mapToLocal() - } else { - historyList - } if (list.isEmpty()) { return if (filters.isEmpty()) { listOf(getEmptyState(hasFilters = false)) @@ -198,20 +193,6 @@ class HistoryListViewModel @Inject constructor( return result } - private suspend fun List.mapToLocal() = coroutineScope { - map { - async { - if (it.manga.isLocal) { - it - } else { - localMangaRepository.findSavedManga(it.manga)?.let { localManga -> - MangaWithHistory(localManga.manga, it.history) - } - } - } - }.awaitAll().filterNotNull() - } - private fun MangaHistory.header(order: ListSortOrder): ListHeader? = when (order) { ListSortOrder.LAST_READ, ListSortOrder.LONG_AGO_READ -> ListHeader(calculateTimeAgo(updatedAt)) diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/local/domain/LocalObserveMapper.kt b/app/src/main/kotlin/org/koitharu/kotatsu/local/domain/LocalObserveMapper.kt new file mode 100644 index 000000000..03e77b252 --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/local/domain/LocalObserveMapper.kt @@ -0,0 +1,55 @@ +package org.koitharu.kotatsu.local.domain + +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.transformLatest +import org.koitharu.kotatsu.core.model.isLocal +import org.koitharu.kotatsu.local.data.LocalMangaRepository +import org.koitharu.kotatsu.parsers.model.Manga + +abstract class LocalObserveMapper( + private val localMangaRepository: LocalMangaRepository, + private val limitStep: Int, +) { + + protected fun observe(limit: Int, observer: (limit: Int) -> Flow>): Flow> { + val floatingLimit = MutableStateFlow(limit) + return floatingLimit.flatMapLatest { l -> + observer(l) + .transformLatest { fullList -> + val mapped = fullList.mapToLocal() + if (mapped.size < limit && fullList.size == l) { + floatingLimit.value += limitStep + } else { + emit(mapped.take(limit)) + } + }.distinctUntilChanged() + + } + } + + private suspend fun List.mapToLocal(): List = coroutineScope { + val dispatcher = Dispatchers.IO.limitedParallelism(6) + map { + async(dispatcher) { + val m = toManga(it) + val mapped = if (m.isLocal) { + m + } else { + localMangaRepository.findSavedManga(m)?.manga + } + mapped?.let { mm -> toResult(it, mm) } + } + }.awaitAll().filterNotNull() + } + + protected abstract fun toManga(e: E): Manga + + protected abstract fun toResult(e: E, manga: Manga): R +}