diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/favourites/data/FavouritesDao.kt b/app/src/main/kotlin/org/koitharu/kotatsu/favourites/data/FavouritesDao.kt index 061731415..13f54b9d6 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/favourites/data/FavouritesDao.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/favourites/data/FavouritesDao.kt @@ -27,15 +27,20 @@ abstract class FavouritesDao { @Query("SELECT * FROM favourites WHERE deleted_at = 0 GROUP BY manga_id ORDER BY created_at DESC LIMIT :limit") abstract suspend fun findLast(limit: Int): List - fun observeAll(order: ListSortOrder): Flow> { + fun observeAll(order: ListSortOrder, limit: Int): Flow> { val orderBy = getOrderBy(order) - - @Language("RoomSql") - val query = SimpleSQLiteQuery( - "SELECT * FROM favourites LEFT JOIN manga ON favourites.manga_id = manga.manga_id " + - "WHERE favourites.deleted_at = 0 GROUP BY favourites.manga_id ORDER BY $orderBy", - ) - return observeAllImpl(query) + val query = buildString { + append( + "SELECT * FROM favourites LEFT JOIN manga ON favourites.manga_id = manga.manga_id " + + "WHERE favourites.deleted_at = 0 GROUP BY favourites.manga_id ORDER BY ", + ) + append(orderBy) + if (limit > 0) { + append(" LIMIT ") + append(limit) + } + } + return observeAllImpl(SimpleSQLiteQuery(query)) } @Transaction @@ -52,16 +57,21 @@ abstract class FavouritesDao { ) abstract suspend fun findAll(categoryId: Long): List - fun observeAll(categoryId: Long, order: ListSortOrder): Flow> { + fun observeAll(categoryId: Long, order: ListSortOrder, limit: Int): Flow> { val orderBy = getOrderBy(order) + val query = buildString { + append( + "SELECT * FROM favourites LEFT JOIN manga ON favourites.manga_id = manga.manga_id " + + "WHERE category_id = ? AND deleted_at = 0 GROUP BY favourites.manga_id ORDER BY ", + ) + append(orderBy) + if (limit > 0) { + append(" LIMIT ") + append(limit) + } + } - @Language("RoomSql") - val query = SimpleSQLiteQuery( - "SELECT * FROM favourites LEFT JOIN manga ON favourites.manga_id = manga.manga_id " + - "WHERE category_id = ? AND deleted_at = 0 GROUP BY favourites.manga_id ORDER BY $orderBy", - arrayOf(categoryId), - ) - return observeAllImpl(query) + return observeAllImpl(SimpleSQLiteQuery(query, arrayOf(categoryId))) } suspend fun findCovers(categoryId: Long, order: ListSortOrder): List { 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 89fa4987c..84387e157 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 @@ -38,8 +38,8 @@ class FavouritesRepository @Inject constructor( return entities.toMangaList() } - fun observeAll(order: ListSortOrder): Flow> { - return db.getFavouritesDao().observeAll(order) + fun observeAll(order: ListSortOrder, limit: Int): Flow> { + return db.getFavouritesDao().observeAll(order, limit) .mapItems { it.toManga() } } @@ -48,14 +48,14 @@ class FavouritesRepository @Inject constructor( return entities.toMangaList() } - fun observeAll(categoryId: Long, order: ListSortOrder): Flow> { - return db.getFavouritesDao().observeAll(categoryId, order) + fun observeAll(categoryId: Long, order: ListSortOrder, limit: Int): Flow> { + return db.getFavouritesDao().observeAll(categoryId, order, limit) .mapItems { it.toManga() } } - fun observeAll(categoryId: Long): Flow> { + fun observeAll(categoryId: Long, limit: Int): Flow> { return observeOrder(categoryId) - .flatMapLatest { order -> observeAll(categoryId, order) } + .flatMapLatest { order -> observeAll(categoryId, order, limit) } } fun observeMangaCount(): Flow { @@ -63,12 +63,6 @@ class FavouritesRepository @Inject constructor( .distinctUntilChanged() } - suspend fun getCategories(): List { - return db.getFavouriteCategoriesDao().findAll().map { - it.toFavouriteCategory() - } - } - fun observeCategories(): Flow> { return db.getFavouriteCategoriesDao().observeAll().mapItems { it.toFavouriteCategory() diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/favourites/ui/list/FavouritesListFragment.kt b/app/src/main/kotlin/org/koitharu/kotatsu/favourites/ui/list/FavouritesListFragment.kt index 7caf9e068..a49bdf217 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/favourites/ui/list/FavouritesListFragment.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/favourites/ui/list/FavouritesListFragment.kt @@ -33,7 +33,7 @@ class FavouritesListFragment : MangaListFragment(), PopupMenu.OnMenuItemClickLis binding.recyclerView.isVP2BugWorkaroundEnabled = true } - override fun onScrolledToEnd() = Unit + override fun onScrolledToEnd() = viewModel.requestMoreItems() override fun onFilterClick(view: View?) { val menu = PopupMenu(view?.context ?: return, view) 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 a8f8e04b5..4bd578710 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 @@ -32,8 +32,11 @@ import org.koitharu.kotatsu.list.ui.model.LoadingState import org.koitharu.kotatsu.list.ui.model.toErrorState import org.koitharu.kotatsu.list.ui.model.toUi import org.koitharu.kotatsu.parsers.model.Manga +import java.util.concurrent.atomic.AtomicBoolean import javax.inject.Inject +private const val PAGE_SIZE = 20 + @HiltViewModel class FavouritesListViewModel @Inject constructor( savedStateHandle: SavedStateHandle, @@ -46,6 +49,8 @@ class FavouritesListViewModel @Inject constructor( val categoryId: Long = savedStateHandle[ARG_CATEGORY_ID] ?: NO_ID private val refreshTrigger = MutableStateFlow(Any()) + private val limit = MutableStateFlow(PAGE_SIZE) + private val isReady = AtomicBoolean(false) override val listMode = settings.observeAsFlow(AppSettings.KEY_LIST_MODE_FAVORITES) { favoritesListMode } .stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, settings.favoritesListMode) @@ -61,13 +66,7 @@ class FavouritesListViewModel @Inject constructor( }.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, null) override val content = combine( - if (categoryId == NO_ID) { - sortOrder.filterNotNull().flatMapLatest { - repository.observeAll(it) - } - } else { - repository.observeAll(categoryId) - }, + observeFavorites(), listMode, refreshTrigger, ) { list, mode, _ -> @@ -85,7 +84,10 @@ class FavouritesListViewModel @Inject constructor( ), ) - else -> list.toUi(mode, listExtraProvider) + else -> { + isReady.set(true) + list.toUi(mode, listExtraProvider) + } } }.catch { emit(listOf(it.toErrorState(canRetry = false))) @@ -126,4 +128,19 @@ class FavouritesListViewModel @Inject constructor( repository.setCategoryOrder(categoryId, order) } } + + fun requestMoreItems() { + if (isReady.compareAndSet(true, false)) { + limit.value += PAGE_SIZE + } + } + + private fun observeFavorites() = if (categoryId == NO_ID) { + combine(sortOrder.filterNotNull(), limit, ::Pair) + .flatMapLatest { repository.observeAll(it.first, it.second) } + } else { + limit.flatMapLatest { + repository.observeAll(categoryId, it) + } + } } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/history/data/HistoryDao.kt b/app/src/main/kotlin/org/koitharu/kotatsu/history/data/HistoryDao.kt index 815b8e8a3..8add1dc3f 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/history/data/HistoryDao.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/history/data/HistoryDao.kt @@ -9,7 +9,6 @@ import androidx.room.Transaction import androidx.sqlite.db.SimpleSQLiteQuery import androidx.sqlite.db.SupportSQLiteQuery import kotlinx.coroutines.flow.Flow -import org.intellij.lang.annotations.Language import org.koitharu.kotatsu.core.db.entity.TagEntity import org.koitharu.kotatsu.list.domain.ListSortOrder @@ -28,8 +27,7 @@ abstract class HistoryDao { @Query("SELECT * FROM history WHERE deleted_at = 0 ORDER BY updated_at DESC LIMIT :limit") abstract fun observeAll(limit: Int): Flow> - // TODO pagination - fun observeAll(order: ListSortOrder): Flow> { + fun observeAll(order: ListSortOrder, limit: Int): Flow> { val orderBy = when (order) { ListSortOrder.LAST_READ -> "history.updated_at DESC" ListSortOrder.LONG_AGO_READ -> "history.updated_at ASC" @@ -43,13 +41,18 @@ abstract class HistoryDao { ListSortOrder.UPDATED -> "IFNULL((SELECT last_chapter_date FROM tracks WHERE tracks.manga_id = manga.manga_id), 0) DESC" else -> throw IllegalArgumentException("Sort order $order is not supported") } - - @Language("RoomSql") - val query = SimpleSQLiteQuery( - "SELECT * FROM history LEFT JOIN manga ON history.manga_id = manga.manga_id " + - "WHERE history.deleted_at = 0 GROUP BY history.manga_id ORDER BY $orderBy", - ) - return observeAllImpl(query) + val query = buildString { + append( + "SELECT * FROM history LEFT JOIN manga ON history.manga_id = manga.manga_id " + + "WHERE history.deleted_at = 0 GROUP BY history.manga_id ORDER BY ", + ) + append(orderBy) + if (limit > 0) { + append(" LIMIT ") + append(limit) + } + } + return observeAllImpl(SimpleSQLiteQuery(query)) } @Query("SELECT manga_id FROM history WHERE deleted_at = 0") 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 15eabac4d..c5736a1f3 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 @@ -74,8 +74,8 @@ class HistoryRepository @Inject constructor( } } - fun observeAllWithHistory(order: ListSortOrder): Flow> { - return db.getHistoryDao().observeAll(order).mapItems { + fun observeAllWithHistory(order: ListSortOrder, limit: Int): Flow> { + return db.getHistoryDao().observeAll(order, limit).mapItems { MangaWithHistory( it.manga.toManga(it.tags.toMangaTags()), it.history.toMangaHistory(), diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/history/ui/HistoryListFragment.kt b/app/src/main/kotlin/org/koitharu/kotatsu/history/ui/HistoryListFragment.kt index ba733b064..4d5a1cac3 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/history/ui/HistoryListFragment.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/history/ui/HistoryListFragment.kt @@ -32,7 +32,7 @@ class HistoryListFragment : MangaListFragment() { viewModel.isStatsEnabled.observe(viewLifecycleOwner, MenuInvalidator(requireActivity())) } - override fun onScrolledToEnd() = Unit + override fun onScrolledToEnd() = viewModel.requestMoreItems() override fun onEmptyActionClick() { startActivity(NetworkManageIntent()) 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 ecee9a70f..e10d0c626 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,6 +3,7 @@ package org.koitharu.kotatsu.history.ui import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.catch @@ -43,8 +44,11 @@ import org.koitharu.kotatsu.list.ui.model.toListModel 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 + @HiltViewModel class HistoryListViewModel @Inject constructor( private val repository: HistoryRepository, @@ -62,8 +66,11 @@ class HistoryListViewModel @Inject constructor( valueProducer = { historySortOrder }, ) - override val listMode = settings.observeAsFlow(AppSettings.KEY_LIST_MODE_HISTORY) { historyListMode } - .stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, settings.historyListMode) + override val listMode = settings.observeAsStateFlow( + scope = viewModelScope + Dispatchers.Default, + key = AppSettings.KEY_LIST_MODE_HISTORY, + valueProducer = { historyListMode }, + ) private val isGroupingEnabled = settings.observeAsFlow( key = AppSettings.KEY_HISTORY_GROUPING, @@ -72,6 +79,9 @@ class HistoryListViewModel @Inject constructor( g && s.isGroupingSupported() } + private val limit = MutableStateFlow(PAGE_SIZE) + private val isReady = AtomicBoolean(false) + val isStatsEnabled = settings.observeAsStateFlow( scope = viewModelScope + Dispatchers.Default, key = AppSettings.KEY_STATS_ENABLED, @@ -79,7 +89,7 @@ class HistoryListViewModel @Inject constructor( ) override val content = combine( - sortOrder.flatMapLatest { repository.observeAllWithHistory(it) }, + observeHistory(), isGroupingEnabled, listMode, networkState, @@ -95,7 +105,10 @@ class HistoryListViewModel @Inject constructor( ), ) - else -> mapList(list, grouped, mode, online, incognito) + else -> { + isReady.set(true) + mapList(list, grouped, mode, online, incognito) + } } }.onStart { loadingCounter.increment() @@ -138,6 +151,15 @@ class HistoryListViewModel @Inject constructor( } } + fun requestMoreItems() { + if (isReady.compareAndSet(true, false)) { + limit.value += PAGE_SIZE + } + } + + private fun observeHistory() = combine(sortOrder, limit, ::Pair) + .flatMapLatest { repository.observeAllWithHistory(it.first, it.second) } + private suspend fun mapList( list: List, grouped: Boolean,