From 65abef12829d2a10b1eb01036b8c6d43725a05dd Mon Sep 17 00:00:00 2001 From: Koitharu Date: Sun, 18 Aug 2024 14:01:29 +0300 Subject: [PATCH] Quick filter in feed and updates lists --- .../kotatsu/core/db/dao/TrackLogsDao.kt | 72 ++++++++++++++++--- .../kotatsu/core/prefs/AppSettings.kt | 4 ++ .../favourites/data/FavouriteCategoriesDao.kt | 3 + .../kotatsu/favourites/data/FavouritesDao.kt | 3 +- .../domain/FavoritesListQuickFilter.kt | 2 +- .../favourites/domain/FavouritesRepository.kt | 6 ++ .../ui/list/FavouritesListViewModel.kt | 4 +- .../kotatsu/history/data/HistoryDao.kt | 3 +- .../history/domain/HistoryListQuickFilter.kt | 2 +- .../history/ui/HistoryListViewModel.kt | 4 +- .../list/domain/MangaListQuickFilter.kt | 21 ++++-- .../kotatsu/list/ui/MangaListViewModel.kt | 4 +- .../kotatsu/list/ui/adapter/ListItemType.kt | 2 +- .../list/ui/adapter/MangaListAdapter.kt | 2 +- .../ui/adapter/TypedListSpacingDecoration.kt | 10 +-- .../kotatsu/tracker/data/TracksDao.kt | 49 ++++++++++++- .../tracker/domain/TrackingRepository.kt | 40 +++++------ .../tracker/domain/UpdatesListQuickFilter.kt | 20 ++++++ .../kotatsu/tracker/ui/feed/FeedFragment.kt | 2 +- .../kotatsu/tracker/ui/feed/FeedViewModel.kt | 26 +++++-- .../tracker/ui/feed/adapter/FeedAdapter.kt | 2 + .../tracker/ui/updates/UpdatesViewModel.kt | 31 ++++++-- app/src/main/res/values/strings.xml | 2 + app/src/main/res/xml/pref_appearance.xml | 6 ++ 24 files changed, 248 insertions(+), 72 deletions(-) create mode 100644 app/src/main/kotlin/org/koitharu/kotatsu/tracker/domain/UpdatesListQuickFilter.kt diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/db/dao/TrackLogsDao.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/db/dao/TrackLogsDao.kt index e9580ce6d..1636eb9ad 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/db/dao/TrackLogsDao.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/db/dao/TrackLogsDao.kt @@ -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> + fun observeAll(limit: Int, filterOptions: Set): Flow> { + 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 + abstract fun observeUnreadCount(): Flow @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> + + 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") + } } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/prefs/AppSettings.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/prefs/AppSettings.kt index 7ca1c78f3..ea451689e 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/prefs/AppSettings.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/prefs/AppSettings.kt @@ -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" diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/favourites/data/FavouriteCategoriesDao.kt b/app/src/main/kotlin/org/koitharu/kotatsu/favourites/data/FavouriteCategoriesDao.kt index d133b0488..4a9d287c7 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/favourites/data/FavouriteCategoriesDao.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/favourites/data/FavouriteCategoriesDao.kt @@ -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 + suspend fun getNextSortKey(): Int { return (getMaxSortKey() ?: 0) + 1 } 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 3e1eeeda5..6f28a466d 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 @@ -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})" diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/favourites/domain/FavoritesListQuickFilter.kt b/app/src/main/kotlin/org/koitharu/kotatsu/favourites/domain/FavoritesListQuickFilter.kt index 8eddca094..d2de06707 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/favourites/domain/FavoritesListQuickFilter.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/favourites/domain/FavoritesListQuickFilter.kt @@ -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) 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 12d3abd0e..b8d216fe1 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 @@ -238,6 +238,12 @@ class FavouritesRepository @Inject constructor( .distinctUntilChanged() } + suspend fun getMostUpdatedCategories(limit: Int): List { + return db.getFavouriteCategoriesDao().getMostUpdatedCategories(limit).map { + it.toFavouriteCategory() + } + } + private suspend fun recoverToFavourites(ids: Collection) { db.withTransaction { for (id in ids) { 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 4babfe2ee..282cf776a 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 @@ -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(list.size + 1) - result += quickFilter.filterItem(filters) + quickFilter.filterItem(filters)?.let(result::add) mangaListMapper.toListModelList(result, list, mode) return result } 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 0ad7ccef9..2cfbca8a8 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 @@ -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" diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/history/domain/HistoryListQuickFilter.kt b/app/src/main/kotlin/org/koitharu/kotatsu/history/domain/HistoryListQuickFilter.kt index 7ed675cfc..aabcd94e1 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/history/domain/HistoryListQuickFilter.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/history/domain/HistoryListQuickFilter.kt @@ -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) 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 79aac0eb9..b3ebe4076 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 @@ -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((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, diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/list/domain/MangaListQuickFilter.kt b/app/src/main/kotlin/org/koitharu/kotatsu/list/domain/MangaListQuickFilter.kt index 0a2f360fc..f0f544b8b 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/list/domain/MangaListQuickFilter.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/list/domain/MangaListQuickFilter.kt @@ -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>(emptySet()) private val availableFilterOptions = SuspendLazy { @@ -43,8 +46,11 @@ abstract class MangaListQuickFilter : QuickFilterListener { suspend fun filterItem( selectedOptions: Set, - ) = 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 } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/MangaListViewModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/MangaListViewModel.kt index 5dcc0c23a..8ea51a48c 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/MangaListViewModel.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/MangaListViewModel.kt @@ -64,7 +64,9 @@ abstract class MangaListViewModel( protected fun observeListModeWithTriggers(): Flow = 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 diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/adapter/ListItemType.kt b/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/adapter/ListItemType.kt index a9a6fba62..6e2ef517d 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/adapter/ListItemType.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/adapter/ListItemType.kt @@ -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, diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/adapter/MangaListAdapter.kt b/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/adapter/MangaListAdapter.kt index 782fec1f9..dd0a5767e 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/adapter/MangaListAdapter.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/adapter/MangaListAdapter.kt @@ -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()) } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/adapter/TypedListSpacingDecoration.kt b/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/adapter/TypedListSpacingDecoration.kt index 0ed80dcbd..b56d5a1af 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/adapter/TypedListSpacingDecoration.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/adapter/TypedListSpacingDecoration.kt @@ -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) diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/tracker/data/TracksDao.kt b/app/src/main/kotlin/org/koitharu/kotatsu/tracker/data/TracksDao.kt index e3820b0da..21ae5e892 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/tracker/data/TracksDao.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/tracker/data/TracksDao.kt @@ -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> - @Transaction - @Query("SELECT * FROM tracks WHERE chapters_new > 0 ORDER BY last_chapter_date DESC LIMIT :limit") - abstract fun observeUpdatedManga(limit: Int): Flow> + fun observeUpdatedManga(limit: Int, filterOptions: Set): Flow> { + 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> + + 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") + } } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/tracker/domain/TrackingRepository.kt b/app/src/main/kotlin/org/koitharu/kotatsu/tracker/domain/TrackingRepository.kt index 6f95b6a31..58bd10e95 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/tracker/domain/TrackingRepository.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/tracker/domain/TrackingRepository.kt @@ -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> { - 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): Flow> { + 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): Flow> { - return limit.flatMapLatest { limitValue -> - db.getTrackLogsDao().observeAll(limitValue) - .mapItems { it.toTrackingLogItem() } - }.onStart { - gcIfNotCalled() - } + fun observeTrackingLog(limit: Int, filterOptions: Set): Flow> { + 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) } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/tracker/domain/UpdatesListQuickFilter.kt b/app/src/main/kotlin/org/koitharu/kotatsu/tracker/domain/UpdatesListQuickFilter.kt new file mode 100644 index 000000000..cee9f326c --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/tracker/domain/UpdatesListQuickFilter.kt @@ -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 = + favouritesRepository.getMostUpdatedCategories( + limit = 4, + ).map { + ListFilterOption.Favorite(it) + } +} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/tracker/ui/feed/FeedFragment.kt b/app/src/main/kotlin/org/koitharu/kotatsu/tracker/ui/feed/FeedFragment.kt index 6e9e112d8..935bbe0db 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/tracker/ui/feed/FeedFragment.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/tracker/ui/feed/FeedFragment.kt @@ -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 diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/tracker/ui/feed/FeedViewModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/tracker/ui/feed/FeedViewModel.kt index 70871c4a1..1d2b9bd37 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/tracker/ui/feed/FeedViewModel.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/tracker/ui/feed/FeedViewModel.kt @@ -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() + + @Suppress("USELESS_CAST") val content = combine( observeHeader(), - repository.observeTrackingLog(limit), - ) { header, list -> - val result = ArrayList((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((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 + }.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 { diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/tracker/ui/feed/adapter/FeedAdapter.kt b/app/src/main/kotlin/org/koitharu/kotatsu/tracker/ui/feed/adapter/FeedAdapter.kt index 15cdeb9dd..4e212ad1f 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/tracker/ui/feed/adapter/FeedAdapter.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/tracker/ui/feed/adapter/FeedAdapter.kt @@ -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? { diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/tracker/ui/updates/UpdatesViewModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/tracker/ui/updates/UpdatesViewModel.kt index e1ab3c5ed..b332edf0a 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/tracker/ui/updates/UpdatesViewModel.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/tracker/ui/updates/UpdatesViewModel.kt @@ -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.toUi(mode: ListMode, grouped: Boolean): List { - val result = ArrayList(if (grouped) (size * 1.4).toInt() else size) + private suspend fun List.toUi( + mode: ListMode, + filters: Set, + grouped: Boolean, + ): List { + val result = ArrayList(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) { diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 2973b7831..82fe8cdd6 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -673,4 +673,6 @@ Incompatible plugin or internal error. Make sure you are using the latest version of the plugin and Kotatsu Connection is OK Invalid proxy configuration + Show quick filters + Provides the ability to filter manga lists by certain parameters diff --git a/app/src/main/res/xml/pref_appearance.xml b/app/src/main/res/xml/pref_appearance.xml index 8acb08a2e..914d354c3 100644 --- a/app/src/main/res/xml/pref_appearance.xml +++ b/app/src/main/res/xml/pref_appearance.xml @@ -38,6 +38,12 @@ android:valueTo="150" app:defaultValue="100" /> + +