Pagination in history and favorites

This commit is contained in:
Koitharu
2024-05-23 12:44:10 +03:00
parent 4c55682552
commit 7347f0ba10
8 changed files with 100 additions and 54 deletions

View File

@@ -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") @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<FavouriteManga> abstract suspend fun findLast(limit: Int): List<FavouriteManga>
fun observeAll(order: ListSortOrder): Flow<List<FavouriteManga>> { fun observeAll(order: ListSortOrder, limit: Int): Flow<List<FavouriteManga>> {
val orderBy = getOrderBy(order) val orderBy = getOrderBy(order)
val query = buildString {
@Language("RoomSql") append(
val query = SimpleSQLiteQuery( "SELECT * FROM favourites LEFT JOIN manga ON favourites.manga_id = manga.manga_id " +
"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 ",
"WHERE favourites.deleted_at = 0 GROUP BY favourites.manga_id ORDER BY $orderBy", )
) append(orderBy)
return observeAllImpl(query) if (limit > 0) {
append(" LIMIT ")
append(limit)
}
}
return observeAllImpl(SimpleSQLiteQuery(query))
} }
@Transaction @Transaction
@@ -52,16 +57,21 @@ abstract class FavouritesDao {
) )
abstract suspend fun findAll(categoryId: Long): List<FavouriteManga> abstract suspend fun findAll(categoryId: Long): List<FavouriteManga>
fun observeAll(categoryId: Long, order: ListSortOrder): Flow<List<FavouriteManga>> { fun observeAll(categoryId: Long, order: ListSortOrder, limit: Int): Flow<List<FavouriteManga>> {
val orderBy = getOrderBy(order) 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") return observeAllImpl(SimpleSQLiteQuery(query, arrayOf<Any>(categoryId)))
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<Any>(categoryId),
)
return observeAllImpl(query)
} }
suspend fun findCovers(categoryId: Long, order: ListSortOrder): List<Cover> { suspend fun findCovers(categoryId: Long, order: ListSortOrder): List<Cover> {

View File

@@ -38,8 +38,8 @@ class FavouritesRepository @Inject constructor(
return entities.toMangaList() return entities.toMangaList()
} }
fun observeAll(order: ListSortOrder): Flow<List<Manga>> { fun observeAll(order: ListSortOrder, limit: Int): Flow<List<Manga>> {
return db.getFavouritesDao().observeAll(order) return db.getFavouritesDao().observeAll(order, limit)
.mapItems { it.toManga() } .mapItems { it.toManga() }
} }
@@ -48,14 +48,14 @@ class FavouritesRepository @Inject constructor(
return entities.toMangaList() return entities.toMangaList()
} }
fun observeAll(categoryId: Long, order: ListSortOrder): Flow<List<Manga>> { fun observeAll(categoryId: Long, order: ListSortOrder, limit: Int): Flow<List<Manga>> {
return db.getFavouritesDao().observeAll(categoryId, order) return db.getFavouritesDao().observeAll(categoryId, order, limit)
.mapItems { it.toManga() } .mapItems { it.toManga() }
} }
fun observeAll(categoryId: Long): Flow<List<Manga>> { fun observeAll(categoryId: Long, limit: Int): Flow<List<Manga>> {
return observeOrder(categoryId) return observeOrder(categoryId)
.flatMapLatest { order -> observeAll(categoryId, order) } .flatMapLatest { order -> observeAll(categoryId, order, limit) }
} }
fun observeMangaCount(): Flow<Int> { fun observeMangaCount(): Flow<Int> {
@@ -63,12 +63,6 @@ class FavouritesRepository @Inject constructor(
.distinctUntilChanged() .distinctUntilChanged()
} }
suspend fun getCategories(): List<FavouriteCategory> {
return db.getFavouriteCategoriesDao().findAll().map {
it.toFavouriteCategory()
}
}
fun observeCategories(): Flow<List<FavouriteCategory>> { fun observeCategories(): Flow<List<FavouriteCategory>> {
return db.getFavouriteCategoriesDao().observeAll().mapItems { return db.getFavouriteCategoriesDao().observeAll().mapItems {
it.toFavouriteCategory() it.toFavouriteCategory()

View File

@@ -33,7 +33,7 @@ class FavouritesListFragment : MangaListFragment(), PopupMenu.OnMenuItemClickLis
binding.recyclerView.isVP2BugWorkaroundEnabled = true binding.recyclerView.isVP2BugWorkaroundEnabled = true
} }
override fun onScrolledToEnd() = Unit override fun onScrolledToEnd() = viewModel.requestMoreItems()
override fun onFilterClick(view: View?) { override fun onFilterClick(view: View?) {
val menu = PopupMenu(view?.context ?: return, view) val menu = PopupMenu(view?.context ?: return, view)

View File

@@ -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.toErrorState
import org.koitharu.kotatsu.list.ui.model.toUi import org.koitharu.kotatsu.list.ui.model.toUi
import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.Manga
import java.util.concurrent.atomic.AtomicBoolean
import javax.inject.Inject import javax.inject.Inject
private const val PAGE_SIZE = 20
@HiltViewModel @HiltViewModel
class FavouritesListViewModel @Inject constructor( class FavouritesListViewModel @Inject constructor(
savedStateHandle: SavedStateHandle, savedStateHandle: SavedStateHandle,
@@ -46,6 +49,8 @@ class FavouritesListViewModel @Inject constructor(
val categoryId: Long = savedStateHandle[ARG_CATEGORY_ID] ?: NO_ID val categoryId: Long = savedStateHandle[ARG_CATEGORY_ID] ?: NO_ID
private val refreshTrigger = MutableStateFlow(Any()) 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 } override val listMode = settings.observeAsFlow(AppSettings.KEY_LIST_MODE_FAVORITES) { favoritesListMode }
.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, settings.favoritesListMode) .stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, settings.favoritesListMode)
@@ -61,13 +66,7 @@ class FavouritesListViewModel @Inject constructor(
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, null) }.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, null)
override val content = combine( override val content = combine(
if (categoryId == NO_ID) { observeFavorites(),
sortOrder.filterNotNull().flatMapLatest {
repository.observeAll(it)
}
} else {
repository.observeAll(categoryId)
},
listMode, listMode,
refreshTrigger, refreshTrigger,
) { list, mode, _ -> ) { list, mode, _ ->
@@ -85,7 +84,10 @@ class FavouritesListViewModel @Inject constructor(
), ),
) )
else -> list.toUi(mode, listExtraProvider) else -> {
isReady.set(true)
list.toUi(mode, listExtraProvider)
}
} }
}.catch { }.catch {
emit(listOf(it.toErrorState(canRetry = false))) emit(listOf(it.toErrorState(canRetry = false)))
@@ -126,4 +128,19 @@ class FavouritesListViewModel @Inject constructor(
repository.setCategoryOrder(categoryId, order) 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)
}
}
} }

View File

@@ -9,7 +9,6 @@ import androidx.room.Transaction
import androidx.sqlite.db.SimpleSQLiteQuery import androidx.sqlite.db.SimpleSQLiteQuery
import androidx.sqlite.db.SupportSQLiteQuery import androidx.sqlite.db.SupportSQLiteQuery
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import org.intellij.lang.annotations.Language
import org.koitharu.kotatsu.core.db.entity.TagEntity import org.koitharu.kotatsu.core.db.entity.TagEntity
import org.koitharu.kotatsu.list.domain.ListSortOrder 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") @Query("SELECT * FROM history WHERE deleted_at = 0 ORDER BY updated_at DESC LIMIT :limit")
abstract fun observeAll(limit: Int): Flow<List<HistoryWithManga>> abstract fun observeAll(limit: Int): Flow<List<HistoryWithManga>>
// TODO pagination fun observeAll(order: ListSortOrder, limit: Int): Flow<List<HistoryWithManga>> {
fun observeAll(order: ListSortOrder): Flow<List<HistoryWithManga>> {
val orderBy = when (order) { val orderBy = when (order) {
ListSortOrder.LAST_READ -> "history.updated_at DESC" ListSortOrder.LAST_READ -> "history.updated_at DESC"
ListSortOrder.LONG_AGO_READ -> "history.updated_at ASC" 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" 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") else -> throw IllegalArgumentException("Sort order $order is not supported")
} }
val query = buildString {
@Language("RoomSql") append(
val query = SimpleSQLiteQuery( "SELECT * FROM history LEFT JOIN manga ON history.manga_id = manga.manga_id " +
"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 ",
"WHERE history.deleted_at = 0 GROUP BY history.manga_id ORDER BY $orderBy", )
) append(orderBy)
return observeAllImpl(query) if (limit > 0) {
append(" LIMIT ")
append(limit)
}
}
return observeAllImpl(SimpleSQLiteQuery(query))
} }
@Query("SELECT manga_id FROM history WHERE deleted_at = 0") @Query("SELECT manga_id FROM history WHERE deleted_at = 0")

View File

@@ -74,8 +74,8 @@ class HistoryRepository @Inject constructor(
} }
} }
fun observeAllWithHistory(order: ListSortOrder): Flow<List<MangaWithHistory>> { fun observeAllWithHistory(order: ListSortOrder, limit: Int): Flow<List<MangaWithHistory>> {
return db.getHistoryDao().observeAll(order).mapItems { return db.getHistoryDao().observeAll(order, limit).mapItems {
MangaWithHistory( MangaWithHistory(
it.manga.toManga(it.tags.toMangaTags()), it.manga.toManga(it.tags.toMangaTags()),
it.history.toMangaHistory(), it.history.toMangaHistory(),

View File

@@ -32,7 +32,7 @@ class HistoryListFragment : MangaListFragment() {
viewModel.isStatsEnabled.observe(viewLifecycleOwner, MenuInvalidator(requireActivity())) viewModel.isStatsEnabled.observe(viewLifecycleOwner, MenuInvalidator(requireActivity()))
} }
override fun onScrolledToEnd() = Unit override fun onScrolledToEnd() = viewModel.requestMoreItems()
override fun onEmptyActionClick() { override fun onEmptyActionClick() {
startActivity(NetworkManageIntent()) startActivity(NetworkManageIntent())

View File

@@ -3,6 +3,7 @@ package org.koitharu.kotatsu.history.ui
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.catch 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.local.data.LocalMangaRepository
import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.Manga
import java.time.Instant import java.time.Instant
import java.util.concurrent.atomic.AtomicBoolean
import javax.inject.Inject import javax.inject.Inject
private const val PAGE_SIZE = 20
@HiltViewModel @HiltViewModel
class HistoryListViewModel @Inject constructor( class HistoryListViewModel @Inject constructor(
private val repository: HistoryRepository, private val repository: HistoryRepository,
@@ -62,8 +66,11 @@ class HistoryListViewModel @Inject constructor(
valueProducer = { historySortOrder }, valueProducer = { historySortOrder },
) )
override val listMode = settings.observeAsFlow(AppSettings.KEY_LIST_MODE_HISTORY) { historyListMode } override val listMode = settings.observeAsStateFlow(
.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, settings.historyListMode) scope = viewModelScope + Dispatchers.Default,
key = AppSettings.KEY_LIST_MODE_HISTORY,
valueProducer = { historyListMode },
)
private val isGroupingEnabled = settings.observeAsFlow( private val isGroupingEnabled = settings.observeAsFlow(
key = AppSettings.KEY_HISTORY_GROUPING, key = AppSettings.KEY_HISTORY_GROUPING,
@@ -72,6 +79,9 @@ class HistoryListViewModel @Inject constructor(
g && s.isGroupingSupported() g && s.isGroupingSupported()
} }
private val limit = MutableStateFlow(PAGE_SIZE)
private val isReady = AtomicBoolean(false)
val isStatsEnabled = settings.observeAsStateFlow( val isStatsEnabled = settings.observeAsStateFlow(
scope = viewModelScope + Dispatchers.Default, scope = viewModelScope + Dispatchers.Default,
key = AppSettings.KEY_STATS_ENABLED, key = AppSettings.KEY_STATS_ENABLED,
@@ -79,7 +89,7 @@ class HistoryListViewModel @Inject constructor(
) )
override val content = combine( override val content = combine(
sortOrder.flatMapLatest { repository.observeAllWithHistory(it) }, observeHistory(),
isGroupingEnabled, isGroupingEnabled,
listMode, listMode,
networkState, 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 { }.onStart {
loadingCounter.increment() 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( private suspend fun mapList(
list: List<MangaWithHistory>, list: List<MangaWithHistory>,
grouped: Boolean, grouped: Boolean,