Pagination in history and favorites
This commit is contained in:
@@ -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> {
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -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(),
|
||||||
|
|||||||
@@ -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())
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user