Quick filter in feed and updates lists
This commit is contained in:
@@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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})"
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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? {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user