Quick filter in feed and updates lists

This commit is contained in:
Koitharu
2024-08-18 14:01:29 +03:00
parent b66d3ee8d4
commit 65abef1282
24 changed files with 248 additions and 72 deletions

View File

@@ -4,36 +4,86 @@ import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import androidx.room.RawQuery
import androidx.room.Transaction
import androidx.sqlite.db.SimpleSQLiteQuery
import androidx.sqlite.db.SupportSQLiteQuery
import kotlinx.coroutines.flow.Flow
import org.koitharu.kotatsu.core.db.entity.toEntity
import org.koitharu.kotatsu.list.domain.ListFilterOption
import org.koitharu.kotatsu.tracker.data.TrackLogEntity
import org.koitharu.kotatsu.tracker.data.TrackLogWithManga
@Dao
interface TrackLogsDao {
abstract class TrackLogsDao {
@Transaction
@Query("SELECT * FROM track_logs ORDER BY created_at DESC LIMIT :limit OFFSET 0")
fun observeAll(limit: Int): Flow<List<TrackLogWithManga>>
fun observeAll(limit: Int, filterOptions: Set<ListFilterOption>): Flow<List<TrackLogWithManga>> {
val query = buildString {
append("SELECT * FROM track_logs")
if (filterOptions.isNotEmpty()) {
append(" WHERE")
var isFirst = true
val groupedOptions = filterOptions.groupBy { it.groupKey }
for ((_, group) in groupedOptions) {
if (group.isEmpty()) {
continue
}
if (isFirst) {
isFirst = false
append(' ')
} else {
append(" AND ")
}
if (group.size > 1) {
group.joinTo(this, separator = " OR ", prefix = "(", postfix = ")") {
it.getCondition()
}
} else {
append(group.single().getCondition())
}
}
}
append(" ORDER BY created_at DESC")
if (limit > 0) {
append(" LIMIT ")
append(limit)
}
}
return observeAllImpl(SimpleSQLiteQuery(query))
}
@Query("SELECT COUNT(*) FROM track_logs WHERE unread = 1")
fun observeUnreadCount(): Flow<Int>
abstract fun observeUnreadCount(): Flow<Int>
@Query("DELETE FROM track_logs")
suspend fun clear()
abstract suspend fun clear()
@Query("UPDATE track_logs SET unread = 0 WHERE id = :id")
suspend fun markAsRead(id: Long)
abstract suspend fun markAsRead(id: Long)
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insert(entity: TrackLogEntity): Long
abstract suspend fun insert(entity: TrackLogEntity): Long
@Query("DELETE FROM track_logs WHERE manga_id NOT IN (SELECT manga_id FROM tracks)")
suspend fun gc()
abstract suspend fun gc()
@Query("DELETE FROM track_logs WHERE id IN (SELECT id FROM track_logs ORDER BY created_at DESC LIMIT 0 OFFSET :size)")
suspend fun trim(size: Int)
abstract suspend fun trim(size: Int)
@Query("SELECT COUNT(*) FROM track_logs")
suspend fun count(): Int
abstract suspend fun count(): Int
@Transaction
@RawQuery(observedEntities = [TrackLogEntity::class])
protected abstract fun observeAllImpl(query: SupportSQLiteQuery): Flow<List<TrackLogWithManga>>
private fun ListFilterOption.getCondition(): String = when (this) {
ListFilterOption.Macro.FAVORITE -> "EXISTS(SELECT * FROM favourites WHERE favourites.manga_id = track_logs.manga_id)"
is ListFilterOption.Favorite -> "EXISTS(SELECT * FROM favourites WHERE favourites.manga_id = track_logs.manga_id AND favourites.category_id = ${category.id})"
ListFilterOption.Macro.COMPLETED -> TODO()
ListFilterOption.Macro.NEW_CHAPTERS -> TODO()
ListFilterOption.Macro.NSFW -> TODO()
is ListFilterOption.Tag -> "EXISTS(SELECT * FROM manga_tags WHERE manga_tags.manga_id = track_logs.manga_id AND tag_id = ${tag.toEntity().id})"
else -> throw IllegalArgumentException("Unsupported option $this")
}
}

View File

@@ -85,6 +85,9 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
get() = prefs.getInt(KEY_GRID_SIZE_PAGES, 100)
set(value) = prefs.edit { putInt(KEY_GRID_SIZE_PAGES, value) }
val isQuickFilterEnabled: Boolean
get() = prefs.getBoolean(KEY_QUICK_FILTER, true)
var historyListMode: ListMode
get() = prefs.getEnumValue(KEY_LIST_MODE_HISTORY, listMode)
set(value) = prefs.edit { putEnumValue(KEY_LIST_MODE_HISTORY, value) }
@@ -696,6 +699,7 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
const val KEY_FEED_HEADER = "feed_header"
const val KEY_SEARCH_SUGGESTION_TYPES = "search_suggest_types"
const val KEY_SOURCES_VERSION = "sources_version"
const val KEY_QUICK_FILTER = "quick_filter"
// keys for non-persistent preferences
const val KEY_APP_VERSION = "app_version"

View File

@@ -51,6 +51,9 @@ abstract class FavouriteCategoriesDao {
@Query("SELECT MAX(sort_key) FROM favourite_categories WHERE deleted_at = 0")
protected abstract suspend fun getMaxSortKey(): Int?
@Query("SELECT favourite_categories.*, (SELECT SUM(chapters_new) FROM tracks WHERE tracks.manga_id IN (SELECT manga_id FROM favourites WHERE favourites.category_id = favourite_categories.category_id)) AS new_chapters FROM favourite_categories WHERE track = 1 AND show_in_lib = 1 AND deleted_at = 0 AND new_chapters > 0 ORDER BY new_chapters DESC LIMIT :limit")
abstract suspend fun getMostUpdatedCategories(limit: Int): List<FavouriteCategoryEntity>
suspend fun getNextSortKey(): Int {
return (getMaxSortKey() ?: 0) + 1
}

View File

@@ -15,6 +15,7 @@ import org.koitharu.kotatsu.core.db.entity.toEntity
import org.koitharu.kotatsu.favourites.domain.model.Cover
import org.koitharu.kotatsu.list.domain.ListFilterOption
import org.koitharu.kotatsu.list.domain.ListSortOrder
import org.koitharu.kotatsu.list.domain.ReadingProgress.Companion.PROGRESS_COMPLETED
@Dao
abstract class FavouritesDao {
@@ -213,7 +214,7 @@ abstract class FavouritesDao {
}
private fun ListFilterOption.getCondition(): String = when (this) {
ListFilterOption.Macro.COMPLETED -> "EXISTS(SELECT * FROM history WHERE history.manga_id = favourites.manga_id AND history.percent >= 0.9999)"
ListFilterOption.Macro.COMPLETED -> "EXISTS(SELECT * FROM history WHERE history.manga_id = favourites.manga_id AND history.percent >= $PROGRESS_COMPLETED)"
ListFilterOption.Macro.NEW_CHAPTERS -> "(SELECT chapters_new FROM tracks WHERE tracks.manga_id = favourites.manga_id) > 0"
ListFilterOption.Macro.NSFW -> "manga.nsfw = 1"
is ListFilterOption.Tag -> "EXISTS(SELECT * FROM manga_tags WHERE favourites.manga_id = manga_tags.manga_id AND tag_id = ${tag.toEntity().id})"

View File

@@ -10,7 +10,7 @@ class FavoritesListQuickFilter @Inject constructor(
private val settings: AppSettings,
private val repository: FavouritesRepository,
networkState: NetworkState,
) : MangaListQuickFilter() {
) : MangaListQuickFilter(settings) {
init {
setFilterOption(ListFilterOption.Downloaded, !networkState.value)

View File

@@ -238,6 +238,12 @@ class FavouritesRepository @Inject constructor(
.distinctUntilChanged()
}
suspend fun getMostUpdatedCategories(limit: Int): List<FavouriteCategory> {
return db.getFavouriteCategoriesDao().getMostUpdatedCategories(limit).map {
it.toFavouriteCategory()
}
}
private suspend fun recoverToFavourites(ids: Collection<Long>) {
db.withTransaction {
for (id in ids) {

View File

@@ -86,7 +86,7 @@ class FavouritesListViewModel @Inject constructor(
list.isEmpty() -> if (filters.isEmpty()) {
listOf(getEmptyState(hasFilters = false))
} else {
listOf(quickFilter.filterItem(filters), getEmptyState(hasFilters = true))
listOfNotNull(quickFilter.filterItem(filters), getEmptyState(hasFilters = true))
}
else -> {
@@ -146,7 +146,7 @@ class FavouritesListViewModel @Inject constructor(
this
}
val result = ArrayList<ListModel>(list.size + 1)
result += quickFilter.filterItem(filters)
quickFilter.filterItem(filters)?.let(result::add)
mangaListMapper.toListModelList(result, list, mode)
return result
}

View File

@@ -13,6 +13,7 @@ import org.koitharu.kotatsu.core.db.entity.TagEntity
import org.koitharu.kotatsu.core.db.entity.toEntity
import org.koitharu.kotatsu.list.domain.ListFilterOption
import org.koitharu.kotatsu.list.domain.ListSortOrder
import org.koitharu.kotatsu.list.domain.ReadingProgress.Companion.PROGRESS_COMPLETED
@Dao
abstract class HistoryDao {
@@ -172,7 +173,7 @@ abstract class HistoryDao {
private fun ListFilterOption.getCondition(): String = when (this) {
ListFilterOption.Downloaded -> throw IllegalArgumentException("Unsupported option $this")
is ListFilterOption.Favorite -> "EXISTS(SELECT * FROM favourites WHERE history.manga_id = favourites.manga_id AND category_id = ${category.id})"
ListFilterOption.Macro.COMPLETED -> "percent >= 0.9999"
ListFilterOption.Macro.COMPLETED -> "percent >= $PROGRESS_COMPLETED"
ListFilterOption.Macro.NEW_CHAPTERS -> "(SELECT chapters_new FROM tracks WHERE tracks.manga_id = history.manga_id) > 0"
ListFilterOption.Macro.FAVORITE -> "EXISTS(SELECT * FROM favourites WHERE history.manga_id = favourites.manga_id)"
ListFilterOption.Macro.NSFW -> "manga.nsfw = 1"

View File

@@ -11,7 +11,7 @@ class HistoryListQuickFilter @Inject constructor(
private val settings: AppSettings,
private val repository: HistoryRepository,
networkState: NetworkState,
) : MangaListQuickFilter() {
) : MangaListQuickFilter(settings) {
init {
setFilterOption(ListFilterOption.Downloaded, !networkState.value)

View File

@@ -101,7 +101,7 @@ class HistoryListViewModel @Inject constructor(
if (filters.isEmpty()) {
listOf(getEmptyState(hasFilters = false))
} else {
listOf(quickFilter.filterItem(filters), getEmptyState(hasFilters = true))
listOfNotNull(quickFilter.filterItem(filters), getEmptyState(hasFilters = true))
}
}
@@ -173,7 +173,7 @@ class HistoryListViewModel @Inject constructor(
historyList
}
val result = ArrayList<ListModel>((if (grouped) (list.size * 1.4).toInt() else list.size) + 2)
result += quickFilter.filterItem(filters)
quickFilter.filterItem(filters)?.let(result::add)
if (isIncognito) {
result += InfoModel(
key = AppSettings.KEY_INCOGNITO_MODE,

View File

@@ -3,11 +3,14 @@ package org.koitharu.kotatsu.list.domain
import androidx.collection.ArraySet
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.ui.widgets.ChipsView
import org.koitharu.kotatsu.list.ui.model.QuickFilter
import org.koitharu.kotatsu.parsers.util.SuspendLazy
abstract class MangaListQuickFilter : QuickFilterListener {
abstract class MangaListQuickFilter(
private val settings: AppSettings,
) : QuickFilterListener {
private val appliedFilter = MutableStateFlow<Set<ListFilterOption>>(emptySet())
private val availableFilterOptions = SuspendLazy {
@@ -43,8 +46,11 @@ abstract class MangaListQuickFilter : QuickFilterListener {
suspend fun filterItem(
selectedOptions: Set<ListFilterOption>,
) = QuickFilter(
items = availableFilterOptions.tryGet().getOrNull()?.map { option ->
): QuickFilter? {
if (!settings.isQuickFilterEnabled) {
return null
}
val availableOptions = availableFilterOptions.tryGet().getOrNull()?.map { option ->
ChipsView.ChipModel(
title = option.titleText,
titleResId = option.titleResId,
@@ -53,8 +59,13 @@ abstract class MangaListQuickFilter : QuickFilterListener {
isChecked = option in selectedOptions,
data = option,
)
}.orEmpty(),
)
}.orEmpty()
return if (availableOptions.isNotEmpty()) {
QuickFilter(availableOptions)
} else {
null
}
}
protected abstract suspend fun getAvailableFilterOptions(): List<ListFilterOption>
}

View File

@@ -64,7 +64,9 @@ abstract class MangaListViewModel(
protected fun observeListModeWithTriggers(): Flow<ListMode> = combine(
listMode,
settings.observe().filter { key ->
key == AppSettings.KEY_PROGRESS_INDICATORS || key == AppSettings.KEY_TRACKER_ENABLED
key == AppSettings.KEY_PROGRESS_INDICATORS
|| key == AppSettings.KEY_TRACKER_ENABLED
|| key == AppSettings.KEY_QUICK_FILTER
}.onStart { emit("") },
) { mode, _ ->
mode

View File

@@ -2,7 +2,7 @@ package org.koitharu.kotatsu.list.ui.adapter
enum class ListItemType {
FILTER_HEADER,
QUICK_FILTER,
FILTER_SORT,
FILTER_TAG,
FILTER_TAG_MULTI,

View File

@@ -24,7 +24,7 @@ open class MangaListAdapter(
addDelegate(ListItemType.STATE_EMPTY, emptyStateListAD(coil, lifecycleOwner, listener))
addDelegate(ListItemType.HINT_EMPTY, emptyHintAD(coil, lifecycleOwner, listener))
addDelegate(ListItemType.HEADER, listHeaderAD(listener))
addDelegate(ListItemType.FILTER_HEADER, quickFilterAD(listener))
addDelegate(ListItemType.QUICK_FILTER, quickFilterAD(listener))
addDelegate(ListItemType.TIP, tipAD(listener))
addDelegate(ListItemType.INFO, infoAD())
}

View File

@@ -32,20 +32,20 @@ class TypedListSpacingDecoration(
ListItemType.FILTER_TAG_MULTI,
ListItemType.FILTER_STATE,
ListItemType.FILTER_LANGUAGE,
ListItemType.FILTER_HEADER,
-> outRect.set(0)
ListItemType.QUICK_FILTER,
-> outRect.set(0)
ListItemType.HEADER,
ListItemType.FEED,
ListItemType.EXPLORE_SOURCE_LIST,
ListItemType.MANGA_SCROBBLING,
ListItemType.MANGA_LIST,
-> outRect.set(0)
-> outRect.set(0)
ListItemType.DOWNLOAD,
ListItemType.HINT_EMPTY,
ListItemType.MANGA_LIST_DETAILED,
-> outRect.set(spacingNormal)
-> outRect.set(spacingNormal)
ListItemType.PAGE_THUMB -> outRect.set(spacingNormal)
ListItemType.MANGA_GRID -> outRect.set(0)
@@ -65,7 +65,7 @@ class TypedListSpacingDecoration(
ListItemType.CHAPTER_LIST,
ListItemType.INFO,
null,
-> outRect.set(0)
-> outRect.set(0)
ListItemType.CHAPTER_GRID -> outRect.set(spacingSmall)

View File

@@ -2,9 +2,14 @@ package org.koitharu.kotatsu.tracker.data
import androidx.room.Dao
import androidx.room.Query
import androidx.room.RawQuery
import androidx.room.Transaction
import androidx.room.Upsert
import androidx.sqlite.db.SimpleSQLiteQuery
import androidx.sqlite.db.SupportSQLiteQuery
import kotlinx.coroutines.flow.Flow
import org.koitharu.kotatsu.core.db.entity.toEntity
import org.koitharu.kotatsu.list.domain.ListFilterOption
@Dao
abstract class TracksDao {
@@ -39,9 +44,33 @@ abstract class TracksDao {
@Query("SELECT * FROM tracks WHERE chapters_new > 0 ORDER BY last_chapter_date DESC")
abstract fun observeUpdatedManga(): Flow<List<MangaWithTrack>>
@Transaction
@Query("SELECT * FROM tracks WHERE chapters_new > 0 ORDER BY last_chapter_date DESC LIMIT :limit")
abstract fun observeUpdatedManga(limit: Int): Flow<List<MangaWithTrack>>
fun observeUpdatedManga(limit: Int, filterOptions: Set<ListFilterOption>): Flow<List<MangaWithTrack>> {
val query = buildString {
append("SELECT * FROM tracks WHERE chapters_new > 0")
if (filterOptions.isNotEmpty()) {
val groupedOptions = filterOptions.groupBy { it.groupKey }
for ((_, group) in groupedOptions) {
if (group.isEmpty()) {
continue
}
append(" AND ")
if (group.size > 1) {
group.joinTo(this, separator = " OR ", prefix = "(", postfix = ")") {
it.getCondition()
}
} else {
append(group.single().getCondition())
}
}
}
append(" ORDER BY last_chapter_date DESC")
if (limit > 0) {
append(" LIMIT ")
append(limit)
}
}
return observeMangaImpl(SimpleSQLiteQuery(query))
}
@Query("DELETE FROM tracks")
abstract suspend fun clear()
@@ -60,4 +89,18 @@ abstract class TracksDao {
@Upsert
abstract suspend fun upsert(entity: TrackEntity)
@Transaction
@RawQuery(observedEntities = [TrackEntity::class])
protected abstract fun observeMangaImpl(query: SupportSQLiteQuery): Flow<List<MangaWithTrack>>
private fun ListFilterOption.getCondition(): String = when (this) {
ListFilterOption.Macro.FAVORITE -> "EXISTS(SELECT * FROM favourites WHERE favourites.manga_id = tracks.manga_id)"
is ListFilterOption.Favorite -> "EXISTS(SELECT * FROM favourites WHERE favourites.manga_id = tracks.manga_id AND favourites.category_id = ${category.id})"
ListFilterOption.Macro.COMPLETED -> TODO()
ListFilterOption.Macro.NEW_CHAPTERS -> TODO()
ListFilterOption.Macro.NSFW -> TODO()
is ListFilterOption.Tag -> "EXISTS(SELECT * FROM manga_tags WHERE manga_tags.manga_id = tracks.manga_id AND tag_id = ${tag.toEntity().id})"
else -> throw IllegalArgumentException("Unsupported option $this")
}
}

View File

@@ -5,7 +5,6 @@ import androidx.room.withTransaction
import dagger.Reusable
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onStart
import org.koitharu.kotatsu.core.db.MangaDatabase
@@ -16,6 +15,7 @@ import org.koitharu.kotatsu.core.util.ext.ifZero
import org.koitharu.kotatsu.core.util.ext.mapItems
import org.koitharu.kotatsu.core.util.ext.toInstantOrNull
import org.koitharu.kotatsu.details.domain.ProgressUpdateUseCase
import org.koitharu.kotatsu.list.domain.ListFilterOption
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.tracker.data.TrackEntity
import org.koitharu.kotatsu.tracker.data.TrackLogEntity
@@ -56,20 +56,17 @@ class TrackingRepository @Inject constructor(
return db.getTrackLogsDao().observeUnreadCount()
}
fun observeUpdatedManga(limit: Int = 0): Flow<List<MangaTracking>> {
return if (limit == 0) {
db.getTracksDao().observeUpdatedManga()
} else {
db.getTracksDao().observeUpdatedManga(limit)
}.mapItems {
MangaTracking(
manga = it.manga.toManga(it.tags.toMangaTags()),
lastChapterId = it.track.lastChapterId,
lastCheck = it.track.lastCheckTime.toInstantOrNull(),
lastChapterDate = it.track.lastChapterDate.toInstantOrNull(),
newChapters = it.track.newChapters,
)
}.distinctUntilChanged()
fun observeUpdatedManga(limit: Int, filterOptions: Set<ListFilterOption>): Flow<List<MangaTracking>> {
return db.getTracksDao().observeUpdatedManga(limit, filterOptions)
.mapItems {
MangaTracking(
manga = it.manga.toManga(it.tags.toMangaTags()),
lastChapterId = it.track.lastChapterId,
lastCheck = it.track.lastCheckTime.toInstantOrNull(),
lastChapterDate = it.track.lastChapterDate.toInstantOrNull(),
newChapters = it.track.newChapters,
)
}.distinctUntilChanged()
.onStart { gcIfNotCalled() }
}
@@ -112,13 +109,10 @@ class TrackingRepository @Inject constructor(
db.getTracksDao().delete(mangaId)
}
fun observeTrackingLog(limit: Flow<Int>): Flow<List<TrackingLogItem>> {
return limit.flatMapLatest { limitValue ->
db.getTrackLogsDao().observeAll(limitValue)
.mapItems { it.toTrackingLogItem() }
}.onStart {
gcIfNotCalled()
}
fun observeTrackingLog(limit: Int, filterOptions: Set<ListFilterOption>): Flow<List<TrackingLogItem>> {
return db.getTrackLogsDao().observeAll(limit, filterOptions)
.mapItems { it.toTrackingLogItem() }
.onStart { gcIfNotCalled() }
}
suspend fun getLogsCount() = db.getTrackLogsDao().count()
@@ -217,7 +211,7 @@ class TrackingRepository @Inject constructor(
size - ids.size
}
suspend fun getOrCreateTrack(mangaId: Long): TrackEntity {
private suspend fun getOrCreateTrack(mangaId: Long): TrackEntity {
return db.getTracksDao().find(mangaId) ?: TrackEntity.create(mangaId)
}

View File

@@ -0,0 +1,20 @@
package org.koitharu.kotatsu.tracker.domain
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.favourites.domain.FavouritesRepository
import org.koitharu.kotatsu.list.domain.ListFilterOption
import org.koitharu.kotatsu.list.domain.MangaListQuickFilter
import javax.inject.Inject
class UpdatesListQuickFilter @Inject constructor(
private val favouritesRepository: FavouritesRepository,
settings: AppSettings,
) : MangaListQuickFilter(settings) {
override suspend fun getAvailableFilterOptions(): List<ListFilterOption> =
favouritesRepository.getMostUpdatedCategories(
limit = 4,
).map {
ListFilterOption.Favorite(it)
}
}

View File

@@ -95,7 +95,7 @@ class FeedFragment :
viewModel.update()
}
override fun onFilterOptionClick(option: ListFilterOption) = Unit
override fun onFilterOptionClick(option: ListFilterOption) = viewModel.toggleFilterOption(option)
override fun onRetryClick(error: Throwable) = Unit

View File

@@ -6,6 +6,7 @@ import kotlinx.coroutines.CoroutineStart
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
@@ -21,11 +22,14 @@ import org.koitharu.kotatsu.core.util.ext.MutableEventFlow
import org.koitharu.kotatsu.core.util.ext.calculateTimeAgo
import org.koitharu.kotatsu.core.util.ext.call
import org.koitharu.kotatsu.list.domain.MangaListMapper
import org.koitharu.kotatsu.list.domain.QuickFilterListener
import org.koitharu.kotatsu.list.ui.model.EmptyState
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.tracker.domain.TrackingRepository
import org.koitharu.kotatsu.tracker.domain.UpdatesListQuickFilter
import org.koitharu.kotatsu.tracker.domain.model.TrackingLogItem
import org.koitharu.kotatsu.tracker.ui.feed.model.FeedItem
import org.koitharu.kotatsu.tracker.ui.feed.model.UpdatedMangaHeader
@@ -42,7 +46,8 @@ class FeedViewModel @Inject constructor(
private val repository: TrackingRepository,
private val scheduler: TrackWorker.Scheduler,
private val mangaListMapper: MangaListMapper,
) : BaseViewModel() {
private val quickFilter: UpdatesListQuickFilter,
) : BaseViewModel(), QuickFilterListener by quickFilter {
private val limit = MutableStateFlow(PAGE_SIZE)
private val isReady = AtomicBoolean(false)
@@ -57,11 +62,16 @@ class FeedViewModel @Inject constructor(
)
val onFeedCleared = MutableEventFlow<Unit>()
@Suppress("USELESS_CAST")
val content = combine(
observeHeader(),
repository.observeTrackingLog(limit),
) { header, list ->
val result = ArrayList<ListModel>((list.size * 1.4).toInt().coerceAtLeast(2))
quickFilter.appliedOptions,
combine(limit, quickFilter.appliedOptions, ::Pair)
.flatMapLatest { repository.observeTrackingLog(it.first, it.second) },
) { header, filters, list ->
val result = ArrayList<ListModel>((list.size * 1.4).toInt().coerceAtLeast(3))
quickFilter.filterItem(filters)?.let(result::add)
if (header != null) {
result += header
}
@@ -76,7 +86,9 @@ class FeedViewModel @Inject constructor(
isReady.set(true)
list.mapListTo(result)
}
result
result as List<ListModel>
}.catch { e ->
emit(listOf(e.toErrorState(canRetry = false)))
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, listOf(LoadingState))
init {
@@ -129,7 +141,9 @@ class FeedViewModel @Inject constructor(
private fun observeHeader() = isHeaderEnabled.flatMapLatest { hasHeader ->
if (hasHeader) {
repository.observeUpdatedManga(10).map { mangaList ->
quickFilter.appliedOptions.flatMapLatest {
repository.observeUpdatedManga(10, it)
}.map { mangaList ->
if (mangaList.isEmpty()) {
null
} else {

View File

@@ -14,6 +14,7 @@ import org.koitharu.kotatsu.list.ui.adapter.errorStateListAD
import org.koitharu.kotatsu.list.ui.adapter.listHeaderAD
import org.koitharu.kotatsu.list.ui.adapter.loadingFooterAD
import org.koitharu.kotatsu.list.ui.adapter.loadingStateAD
import org.koitharu.kotatsu.list.ui.adapter.quickFilterAD
import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.list.ui.size.ItemSizeResolver
import org.koitharu.kotatsu.tracker.ui.feed.model.FeedItem
@@ -44,6 +45,7 @@ class FeedAdapter(
addDelegate(ListItemType.STATE_ERROR, errorStateListAD(listener))
addDelegate(ListItemType.HEADER, listHeaderAD(listener))
addDelegate(ListItemType.STATE_EMPTY, emptyStateListAD(coil, lifecycleOwner, listener))
addDelegate(ListItemType.QUICK_FILTER, quickFilterAD(listener))
}
override fun getSectionText(context: Context, position: Int): CharSequence? {

View File

@@ -6,6 +6,7 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.SharingStarted
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
@@ -17,7 +18,9 @@ import org.koitharu.kotatsu.core.ui.model.DateTimeAgo
import org.koitharu.kotatsu.core.util.ext.calculateTimeAgo
import org.koitharu.kotatsu.core.util.ext.onFirst
import org.koitharu.kotatsu.download.ui.worker.DownloadWorker
import org.koitharu.kotatsu.list.domain.ListFilterOption
import org.koitharu.kotatsu.list.domain.MangaListMapper
import org.koitharu.kotatsu.list.domain.QuickFilterListener
import org.koitharu.kotatsu.list.ui.MangaListViewModel
import org.koitharu.kotatsu.list.ui.model.EmptyState
import org.koitharu.kotatsu.list.ui.model.ListHeader
@@ -25,6 +28,7 @@ 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.tracker.domain.TrackingRepository
import org.koitharu.kotatsu.tracker.domain.UpdatesListQuickFilter
import org.koitharu.kotatsu.tracker.domain.model.MangaTracking
import javax.inject.Inject
@@ -33,16 +37,24 @@ class UpdatesViewModel @Inject constructor(
private val repository: TrackingRepository,
settings: AppSettings,
private val mangaListMapper: MangaListMapper,
private val quickFilter: UpdatesListQuickFilter,
downloadScheduler: DownloadWorker.Scheduler,
) : MangaListViewModel(settings, downloadScheduler) {
) : MangaListViewModel(settings, downloadScheduler), QuickFilterListener by quickFilter {
override val content = combine(
repository.observeUpdatedManga(),
quickFilter.appliedOptions.flatMapLatest { filterOptions ->
repository.observeUpdatedManga(
limit = 0,
filterOptions = filterOptions,
)
},
quickFilter.appliedOptions,
settings.observeAsFlow(AppSettings.KEY_UPDATED_GROUPING) { isUpdatedGroupingEnabled },
observeListModeWithTriggers(),
) { mangaList, grouping, mode ->
) { mangaList, filters, grouping, mode ->
when {
mangaList.isEmpty() -> listOf(
mangaList.isEmpty() -> listOfNotNull(
quickFilter.filterItem(filters),
EmptyState(
icon = R.drawable.ic_empty_history,
textPrimary = R.string.text_history_holder_primary,
@@ -51,7 +63,7 @@ class UpdatesViewModel @Inject constructor(
),
)
else -> mangaList.toUi(mode, grouping)
else -> mangaList.toUi(mode, filters, grouping)
}
}.onStart {
loadingCounter.increment()
@@ -77,8 +89,13 @@ class UpdatesViewModel @Inject constructor(
}
}
private suspend fun List<MangaTracking>.toUi(mode: ListMode, grouped: Boolean): List<ListModel> {
val result = ArrayList<ListModel>(if (grouped) (size * 1.4).toInt() else size)
private suspend fun List<MangaTracking>.toUi(
mode: ListMode,
filters: Set<ListFilterOption>,
grouped: Boolean,
): List<ListModel> {
val result = ArrayList<ListModel>(if (grouped) (size * 1.4).toInt() else size + 1)
quickFilter.filterItem(filters)?.let(result::add)
var prevHeader: DateTimeAgo? = null
for (item in this) {
if (grouped) {

View File

@@ -673,4 +673,6 @@
<string name="plugin_incompatible">Incompatible plugin or internal error. Make sure you are using the latest version of the plugin and Kotatsu</string>
<string name="connection_ok">Connection is OK</string>
<string name="invalid_proxy_configuration">Invalid proxy configuration</string>
<string name="show_quick_filters">Show quick filters</string>
<string name="show_quick_filters_summary">Provides the ability to filter manga lists by certain parameters</string>
</resources>

View File

@@ -38,6 +38,12 @@
android:valueTo="150"
app:defaultValue="100" />
<SwitchPreferenceCompat
android:defaultValue="true"
android:key="quick_filter"
android:summary="@string/show_quick_filters_summary"
android:title="@string/show_quick_filters" />
<ListPreference
android:entries="@array/progress_indicators"
android:key="progress_indicators"