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.Insert
|
||||||
import androidx.room.OnConflictStrategy
|
import androidx.room.OnConflictStrategy
|
||||||
import androidx.room.Query
|
import androidx.room.Query
|
||||||
|
import androidx.room.RawQuery
|
||||||
import androidx.room.Transaction
|
import androidx.room.Transaction
|
||||||
|
import androidx.sqlite.db.SimpleSQLiteQuery
|
||||||
|
import androidx.sqlite.db.SupportSQLiteQuery
|
||||||
import kotlinx.coroutines.flow.Flow
|
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.TrackLogEntity
|
||||||
import org.koitharu.kotatsu.tracker.data.TrackLogWithManga
|
import org.koitharu.kotatsu.tracker.data.TrackLogWithManga
|
||||||
|
|
||||||
@Dao
|
@Dao
|
||||||
interface TrackLogsDao {
|
abstract class TrackLogsDao {
|
||||||
|
|
||||||
@Transaction
|
fun observeAll(limit: Int, filterOptions: Set<ListFilterOption>): Flow<List<TrackLogWithManga>> {
|
||||||
@Query("SELECT * FROM track_logs ORDER BY created_at DESC LIMIT :limit OFFSET 0")
|
val query = buildString {
|
||||||
fun observeAll(limit: Int): Flow<List<TrackLogWithManga>>
|
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")
|
@Query("SELECT COUNT(*) FROM track_logs WHERE unread = 1")
|
||||||
fun observeUnreadCount(): Flow<Int>
|
abstract fun observeUnreadCount(): Flow<Int>
|
||||||
|
|
||||||
@Query("DELETE FROM track_logs")
|
@Query("DELETE FROM track_logs")
|
||||||
suspend fun clear()
|
abstract suspend fun clear()
|
||||||
|
|
||||||
@Query("UPDATE track_logs SET unread = 0 WHERE id = :id")
|
@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)
|
@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)")
|
@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)")
|
@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")
|
@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)
|
get() = prefs.getInt(KEY_GRID_SIZE_PAGES, 100)
|
||||||
set(value) = prefs.edit { putInt(KEY_GRID_SIZE_PAGES, value) }
|
set(value) = prefs.edit { putInt(KEY_GRID_SIZE_PAGES, value) }
|
||||||
|
|
||||||
|
val isQuickFilterEnabled: Boolean
|
||||||
|
get() = prefs.getBoolean(KEY_QUICK_FILTER, true)
|
||||||
|
|
||||||
var historyListMode: ListMode
|
var historyListMode: ListMode
|
||||||
get() = prefs.getEnumValue(KEY_LIST_MODE_HISTORY, listMode)
|
get() = prefs.getEnumValue(KEY_LIST_MODE_HISTORY, listMode)
|
||||||
set(value) = prefs.edit { putEnumValue(KEY_LIST_MODE_HISTORY, value) }
|
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_FEED_HEADER = "feed_header"
|
||||||
const val KEY_SEARCH_SUGGESTION_TYPES = "search_suggest_types"
|
const val KEY_SEARCH_SUGGESTION_TYPES = "search_suggest_types"
|
||||||
const val KEY_SOURCES_VERSION = "sources_version"
|
const val KEY_SOURCES_VERSION = "sources_version"
|
||||||
|
const val KEY_QUICK_FILTER = "quick_filter"
|
||||||
|
|
||||||
// keys for non-persistent preferences
|
// keys for non-persistent preferences
|
||||||
const val KEY_APP_VERSION = "app_version"
|
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")
|
@Query("SELECT MAX(sort_key) FROM favourite_categories WHERE deleted_at = 0")
|
||||||
protected abstract suspend fun getMaxSortKey(): Int?
|
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 {
|
suspend fun getNextSortKey(): Int {
|
||||||
return (getMaxSortKey() ?: 0) + 1
|
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.favourites.domain.model.Cover
|
||||||
import org.koitharu.kotatsu.list.domain.ListFilterOption
|
import org.koitharu.kotatsu.list.domain.ListFilterOption
|
||||||
import org.koitharu.kotatsu.list.domain.ListSortOrder
|
import org.koitharu.kotatsu.list.domain.ListSortOrder
|
||||||
|
import org.koitharu.kotatsu.list.domain.ReadingProgress.Companion.PROGRESS_COMPLETED
|
||||||
|
|
||||||
@Dao
|
@Dao
|
||||||
abstract class FavouritesDao {
|
abstract class FavouritesDao {
|
||||||
@@ -213,7 +214,7 @@ abstract class FavouritesDao {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun ListFilterOption.getCondition(): String = when (this) {
|
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.NEW_CHAPTERS -> "(SELECT chapters_new FROM tracks WHERE tracks.manga_id = favourites.manga_id) > 0"
|
||||||
ListFilterOption.Macro.NSFW -> "manga.nsfw = 1"
|
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})"
|
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 settings: AppSettings,
|
||||||
private val repository: FavouritesRepository,
|
private val repository: FavouritesRepository,
|
||||||
networkState: NetworkState,
|
networkState: NetworkState,
|
||||||
) : MangaListQuickFilter() {
|
) : MangaListQuickFilter(settings) {
|
||||||
|
|
||||||
init {
|
init {
|
||||||
setFilterOption(ListFilterOption.Downloaded, !networkState.value)
|
setFilterOption(ListFilterOption.Downloaded, !networkState.value)
|
||||||
|
|||||||
@@ -238,6 +238,12 @@ class FavouritesRepository @Inject constructor(
|
|||||||
.distinctUntilChanged()
|
.distinctUntilChanged()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun getMostUpdatedCategories(limit: Int): List<FavouriteCategory> {
|
||||||
|
return db.getFavouriteCategoriesDao().getMostUpdatedCategories(limit).map {
|
||||||
|
it.toFavouriteCategory()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private suspend fun recoverToFavourites(ids: Collection<Long>) {
|
private suspend fun recoverToFavourites(ids: Collection<Long>) {
|
||||||
db.withTransaction {
|
db.withTransaction {
|
||||||
for (id in ids) {
|
for (id in ids) {
|
||||||
|
|||||||
@@ -86,7 +86,7 @@ class FavouritesListViewModel @Inject constructor(
|
|||||||
list.isEmpty() -> if (filters.isEmpty()) {
|
list.isEmpty() -> if (filters.isEmpty()) {
|
||||||
listOf(getEmptyState(hasFilters = false))
|
listOf(getEmptyState(hasFilters = false))
|
||||||
} else {
|
} else {
|
||||||
listOf(quickFilter.filterItem(filters), getEmptyState(hasFilters = true))
|
listOfNotNull(quickFilter.filterItem(filters), getEmptyState(hasFilters = true))
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
@@ -146,7 +146,7 @@ class FavouritesListViewModel @Inject constructor(
|
|||||||
this
|
this
|
||||||
}
|
}
|
||||||
val result = ArrayList<ListModel>(list.size + 1)
|
val result = ArrayList<ListModel>(list.size + 1)
|
||||||
result += quickFilter.filterItem(filters)
|
quickFilter.filterItem(filters)?.let(result::add)
|
||||||
mangaListMapper.toListModelList(result, list, mode)
|
mangaListMapper.toListModelList(result, list, mode)
|
||||||
return result
|
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.core.db.entity.toEntity
|
||||||
import org.koitharu.kotatsu.list.domain.ListFilterOption
|
import org.koitharu.kotatsu.list.domain.ListFilterOption
|
||||||
import org.koitharu.kotatsu.list.domain.ListSortOrder
|
import org.koitharu.kotatsu.list.domain.ListSortOrder
|
||||||
|
import org.koitharu.kotatsu.list.domain.ReadingProgress.Companion.PROGRESS_COMPLETED
|
||||||
|
|
||||||
@Dao
|
@Dao
|
||||||
abstract class HistoryDao {
|
abstract class HistoryDao {
|
||||||
@@ -172,7 +173,7 @@ abstract class HistoryDao {
|
|||||||
private fun ListFilterOption.getCondition(): String = when (this) {
|
private fun ListFilterOption.getCondition(): String = when (this) {
|
||||||
ListFilterOption.Downloaded -> throw IllegalArgumentException("Unsupported option $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})"
|
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.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.FAVORITE -> "EXISTS(SELECT * FROM favourites WHERE history.manga_id = favourites.manga_id)"
|
||||||
ListFilterOption.Macro.NSFW -> "manga.nsfw = 1"
|
ListFilterOption.Macro.NSFW -> "manga.nsfw = 1"
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ class HistoryListQuickFilter @Inject constructor(
|
|||||||
private val settings: AppSettings,
|
private val settings: AppSettings,
|
||||||
private val repository: HistoryRepository,
|
private val repository: HistoryRepository,
|
||||||
networkState: NetworkState,
|
networkState: NetworkState,
|
||||||
) : MangaListQuickFilter() {
|
) : MangaListQuickFilter(settings) {
|
||||||
|
|
||||||
init {
|
init {
|
||||||
setFilterOption(ListFilterOption.Downloaded, !networkState.value)
|
setFilterOption(ListFilterOption.Downloaded, !networkState.value)
|
||||||
|
|||||||
@@ -101,7 +101,7 @@ class HistoryListViewModel @Inject constructor(
|
|||||||
if (filters.isEmpty()) {
|
if (filters.isEmpty()) {
|
||||||
listOf(getEmptyState(hasFilters = false))
|
listOf(getEmptyState(hasFilters = false))
|
||||||
} else {
|
} else {
|
||||||
listOf(quickFilter.filterItem(filters), getEmptyState(hasFilters = true))
|
listOfNotNull(quickFilter.filterItem(filters), getEmptyState(hasFilters = true))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -173,7 +173,7 @@ class HistoryListViewModel @Inject constructor(
|
|||||||
historyList
|
historyList
|
||||||
}
|
}
|
||||||
val result = ArrayList<ListModel>((if (grouped) (list.size * 1.4).toInt() else list.size) + 2)
|
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) {
|
if (isIncognito) {
|
||||||
result += InfoModel(
|
result += InfoModel(
|
||||||
key = AppSettings.KEY_INCOGNITO_MODE,
|
key = AppSettings.KEY_INCOGNITO_MODE,
|
||||||
|
|||||||
@@ -3,11 +3,14 @@ package org.koitharu.kotatsu.list.domain
|
|||||||
import androidx.collection.ArraySet
|
import androidx.collection.ArraySet
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||||
import org.koitharu.kotatsu.core.ui.widgets.ChipsView
|
import org.koitharu.kotatsu.core.ui.widgets.ChipsView
|
||||||
import org.koitharu.kotatsu.list.ui.model.QuickFilter
|
import org.koitharu.kotatsu.list.ui.model.QuickFilter
|
||||||
import org.koitharu.kotatsu.parsers.util.SuspendLazy
|
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 appliedFilter = MutableStateFlow<Set<ListFilterOption>>(emptySet())
|
||||||
private val availableFilterOptions = SuspendLazy {
|
private val availableFilterOptions = SuspendLazy {
|
||||||
@@ -43,8 +46,11 @@ abstract class MangaListQuickFilter : QuickFilterListener {
|
|||||||
|
|
||||||
suspend fun filterItem(
|
suspend fun filterItem(
|
||||||
selectedOptions: Set<ListFilterOption>,
|
selectedOptions: Set<ListFilterOption>,
|
||||||
) = QuickFilter(
|
): QuickFilter? {
|
||||||
items = availableFilterOptions.tryGet().getOrNull()?.map { option ->
|
if (!settings.isQuickFilterEnabled) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
val availableOptions = availableFilterOptions.tryGet().getOrNull()?.map { option ->
|
||||||
ChipsView.ChipModel(
|
ChipsView.ChipModel(
|
||||||
title = option.titleText,
|
title = option.titleText,
|
||||||
titleResId = option.titleResId,
|
titleResId = option.titleResId,
|
||||||
@@ -53,8 +59,13 @@ abstract class MangaListQuickFilter : QuickFilterListener {
|
|||||||
isChecked = option in selectedOptions,
|
isChecked = option in selectedOptions,
|
||||||
data = option,
|
data = option,
|
||||||
)
|
)
|
||||||
}.orEmpty(),
|
}.orEmpty()
|
||||||
)
|
return if (availableOptions.isNotEmpty()) {
|
||||||
|
QuickFilter(availableOptions)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
protected abstract suspend fun getAvailableFilterOptions(): List<ListFilterOption>
|
protected abstract suspend fun getAvailableFilterOptions(): List<ListFilterOption>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -64,7 +64,9 @@ abstract class MangaListViewModel(
|
|||||||
protected fun observeListModeWithTriggers(): Flow<ListMode> = combine(
|
protected fun observeListModeWithTriggers(): Flow<ListMode> = combine(
|
||||||
listMode,
|
listMode,
|
||||||
settings.observe().filter { key ->
|
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("") },
|
}.onStart { emit("") },
|
||||||
) { mode, _ ->
|
) { mode, _ ->
|
||||||
mode
|
mode
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ package org.koitharu.kotatsu.list.ui.adapter
|
|||||||
|
|
||||||
enum class ListItemType {
|
enum class ListItemType {
|
||||||
|
|
||||||
FILTER_HEADER,
|
QUICK_FILTER,
|
||||||
FILTER_SORT,
|
FILTER_SORT,
|
||||||
FILTER_TAG,
|
FILTER_TAG,
|
||||||
FILTER_TAG_MULTI,
|
FILTER_TAG_MULTI,
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ open class MangaListAdapter(
|
|||||||
addDelegate(ListItemType.STATE_EMPTY, emptyStateListAD(coil, lifecycleOwner, listener))
|
addDelegate(ListItemType.STATE_EMPTY, emptyStateListAD(coil, lifecycleOwner, listener))
|
||||||
addDelegate(ListItemType.HINT_EMPTY, emptyHintAD(coil, lifecycleOwner, listener))
|
addDelegate(ListItemType.HINT_EMPTY, emptyHintAD(coil, lifecycleOwner, listener))
|
||||||
addDelegate(ListItemType.HEADER, listHeaderAD(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.TIP, tipAD(listener))
|
||||||
addDelegate(ListItemType.INFO, infoAD())
|
addDelegate(ListItemType.INFO, infoAD())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,20 +32,20 @@ class TypedListSpacingDecoration(
|
|||||||
ListItemType.FILTER_TAG_MULTI,
|
ListItemType.FILTER_TAG_MULTI,
|
||||||
ListItemType.FILTER_STATE,
|
ListItemType.FILTER_STATE,
|
||||||
ListItemType.FILTER_LANGUAGE,
|
ListItemType.FILTER_LANGUAGE,
|
||||||
ListItemType.FILTER_HEADER,
|
ListItemType.QUICK_FILTER,
|
||||||
-> outRect.set(0)
|
-> outRect.set(0)
|
||||||
|
|
||||||
ListItemType.HEADER,
|
ListItemType.HEADER,
|
||||||
ListItemType.FEED,
|
ListItemType.FEED,
|
||||||
ListItemType.EXPLORE_SOURCE_LIST,
|
ListItemType.EXPLORE_SOURCE_LIST,
|
||||||
ListItemType.MANGA_SCROBBLING,
|
ListItemType.MANGA_SCROBBLING,
|
||||||
ListItemType.MANGA_LIST,
|
ListItemType.MANGA_LIST,
|
||||||
-> outRect.set(0)
|
-> outRect.set(0)
|
||||||
|
|
||||||
ListItemType.DOWNLOAD,
|
ListItemType.DOWNLOAD,
|
||||||
ListItemType.HINT_EMPTY,
|
ListItemType.HINT_EMPTY,
|
||||||
ListItemType.MANGA_LIST_DETAILED,
|
ListItemType.MANGA_LIST_DETAILED,
|
||||||
-> outRect.set(spacingNormal)
|
-> outRect.set(spacingNormal)
|
||||||
|
|
||||||
ListItemType.PAGE_THUMB -> outRect.set(spacingNormal)
|
ListItemType.PAGE_THUMB -> outRect.set(spacingNormal)
|
||||||
ListItemType.MANGA_GRID -> outRect.set(0)
|
ListItemType.MANGA_GRID -> outRect.set(0)
|
||||||
@@ -65,7 +65,7 @@ class TypedListSpacingDecoration(
|
|||||||
ListItemType.CHAPTER_LIST,
|
ListItemType.CHAPTER_LIST,
|
||||||
ListItemType.INFO,
|
ListItemType.INFO,
|
||||||
null,
|
null,
|
||||||
-> outRect.set(0)
|
-> outRect.set(0)
|
||||||
|
|
||||||
ListItemType.CHAPTER_GRID -> outRect.set(spacingSmall)
|
ListItemType.CHAPTER_GRID -> outRect.set(spacingSmall)
|
||||||
|
|
||||||
|
|||||||
@@ -2,9 +2,14 @@ package org.koitharu.kotatsu.tracker.data
|
|||||||
|
|
||||||
import androidx.room.Dao
|
import androidx.room.Dao
|
||||||
import androidx.room.Query
|
import androidx.room.Query
|
||||||
|
import androidx.room.RawQuery
|
||||||
import androidx.room.Transaction
|
import androidx.room.Transaction
|
||||||
import androidx.room.Upsert
|
import androidx.room.Upsert
|
||||||
|
import androidx.sqlite.db.SimpleSQLiteQuery
|
||||||
|
import androidx.sqlite.db.SupportSQLiteQuery
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import org.koitharu.kotatsu.core.db.entity.toEntity
|
||||||
|
import org.koitharu.kotatsu.list.domain.ListFilterOption
|
||||||
|
|
||||||
@Dao
|
@Dao
|
||||||
abstract class TracksDao {
|
abstract class TracksDao {
|
||||||
@@ -39,9 +44,33 @@ abstract class TracksDao {
|
|||||||
@Query("SELECT * FROM tracks WHERE chapters_new > 0 ORDER BY last_chapter_date DESC")
|
@Query("SELECT * FROM tracks WHERE chapters_new > 0 ORDER BY last_chapter_date DESC")
|
||||||
abstract fun observeUpdatedManga(): Flow<List<MangaWithTrack>>
|
abstract fun observeUpdatedManga(): Flow<List<MangaWithTrack>>
|
||||||
|
|
||||||
@Transaction
|
fun observeUpdatedManga(limit: Int, filterOptions: Set<ListFilterOption>): Flow<List<MangaWithTrack>> {
|
||||||
@Query("SELECT * FROM tracks WHERE chapters_new > 0 ORDER BY last_chapter_date DESC LIMIT :limit")
|
val query = buildString {
|
||||||
abstract fun observeUpdatedManga(limit: Int): Flow<List<MangaWithTrack>>
|
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")
|
@Query("DELETE FROM tracks")
|
||||||
abstract suspend fun clear()
|
abstract suspend fun clear()
|
||||||
@@ -60,4 +89,18 @@ abstract class TracksDao {
|
|||||||
|
|
||||||
@Upsert
|
@Upsert
|
||||||
abstract suspend fun upsert(entity: TrackEntity)
|
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 dagger.Reusable
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
import kotlinx.coroutines.flow.flatMapLatest
|
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.flow.onStart
|
import kotlinx.coroutines.flow.onStart
|
||||||
import org.koitharu.kotatsu.core.db.MangaDatabase
|
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.mapItems
|
||||||
import org.koitharu.kotatsu.core.util.ext.toInstantOrNull
|
import org.koitharu.kotatsu.core.util.ext.toInstantOrNull
|
||||||
import org.koitharu.kotatsu.details.domain.ProgressUpdateUseCase
|
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.parsers.model.Manga
|
||||||
import org.koitharu.kotatsu.tracker.data.TrackEntity
|
import org.koitharu.kotatsu.tracker.data.TrackEntity
|
||||||
import org.koitharu.kotatsu.tracker.data.TrackLogEntity
|
import org.koitharu.kotatsu.tracker.data.TrackLogEntity
|
||||||
@@ -56,20 +56,17 @@ class TrackingRepository @Inject constructor(
|
|||||||
return db.getTrackLogsDao().observeUnreadCount()
|
return db.getTrackLogsDao().observeUnreadCount()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun observeUpdatedManga(limit: Int = 0): Flow<List<MangaTracking>> {
|
fun observeUpdatedManga(limit: Int, filterOptions: Set<ListFilterOption>): Flow<List<MangaTracking>> {
|
||||||
return if (limit == 0) {
|
return db.getTracksDao().observeUpdatedManga(limit, filterOptions)
|
||||||
db.getTracksDao().observeUpdatedManga()
|
.mapItems {
|
||||||
} else {
|
MangaTracking(
|
||||||
db.getTracksDao().observeUpdatedManga(limit)
|
manga = it.manga.toManga(it.tags.toMangaTags()),
|
||||||
}.mapItems {
|
lastChapterId = it.track.lastChapterId,
|
||||||
MangaTracking(
|
lastCheck = it.track.lastCheckTime.toInstantOrNull(),
|
||||||
manga = it.manga.toManga(it.tags.toMangaTags()),
|
lastChapterDate = it.track.lastChapterDate.toInstantOrNull(),
|
||||||
lastChapterId = it.track.lastChapterId,
|
newChapters = it.track.newChapters,
|
||||||
lastCheck = it.track.lastCheckTime.toInstantOrNull(),
|
)
|
||||||
lastChapterDate = it.track.lastChapterDate.toInstantOrNull(),
|
}.distinctUntilChanged()
|
||||||
newChapters = it.track.newChapters,
|
|
||||||
)
|
|
||||||
}.distinctUntilChanged()
|
|
||||||
.onStart { gcIfNotCalled() }
|
.onStart { gcIfNotCalled() }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -112,13 +109,10 @@ class TrackingRepository @Inject constructor(
|
|||||||
db.getTracksDao().delete(mangaId)
|
db.getTracksDao().delete(mangaId)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun observeTrackingLog(limit: Flow<Int>): Flow<List<TrackingLogItem>> {
|
fun observeTrackingLog(limit: Int, filterOptions: Set<ListFilterOption>): Flow<List<TrackingLogItem>> {
|
||||||
return limit.flatMapLatest { limitValue ->
|
return db.getTrackLogsDao().observeAll(limit, filterOptions)
|
||||||
db.getTrackLogsDao().observeAll(limitValue)
|
.mapItems { it.toTrackingLogItem() }
|
||||||
.mapItems { it.toTrackingLogItem() }
|
.onStart { gcIfNotCalled() }
|
||||||
}.onStart {
|
|
||||||
gcIfNotCalled()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun getLogsCount() = db.getTrackLogsDao().count()
|
suspend fun getLogsCount() = db.getTrackLogsDao().count()
|
||||||
@@ -217,7 +211,7 @@ class TrackingRepository @Inject constructor(
|
|||||||
size - ids.size
|
size - ids.size
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun getOrCreateTrack(mangaId: Long): TrackEntity {
|
private suspend fun getOrCreateTrack(mangaId: Long): TrackEntity {
|
||||||
return db.getTracksDao().find(mangaId) ?: TrackEntity.create(mangaId)
|
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()
|
viewModel.update()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onFilterOptionClick(option: ListFilterOption) = Unit
|
override fun onFilterOptionClick(option: ListFilterOption) = viewModel.toggleFilterOption(option)
|
||||||
|
|
||||||
override fun onRetryClick(error: Throwable) = Unit
|
override fun onRetryClick(error: Throwable) = Unit
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import kotlinx.coroutines.CoroutineStart
|
|||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.SharingStarted
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
|
import kotlinx.coroutines.flow.catch
|
||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
import kotlinx.coroutines.flow.flatMapLatest
|
import kotlinx.coroutines.flow.flatMapLatest
|
||||||
import kotlinx.coroutines.flow.flowOf
|
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.calculateTimeAgo
|
||||||
import org.koitharu.kotatsu.core.util.ext.call
|
import org.koitharu.kotatsu.core.util.ext.call
|
||||||
import org.koitharu.kotatsu.list.domain.MangaListMapper
|
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.EmptyState
|
||||||
import org.koitharu.kotatsu.list.ui.model.ListHeader
|
import org.koitharu.kotatsu.list.ui.model.ListHeader
|
||||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||||
import org.koitharu.kotatsu.list.ui.model.LoadingState
|
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.TrackingRepository
|
||||||
|
import org.koitharu.kotatsu.tracker.domain.UpdatesListQuickFilter
|
||||||
import org.koitharu.kotatsu.tracker.domain.model.TrackingLogItem
|
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.FeedItem
|
||||||
import org.koitharu.kotatsu.tracker.ui.feed.model.UpdatedMangaHeader
|
import org.koitharu.kotatsu.tracker.ui.feed.model.UpdatedMangaHeader
|
||||||
@@ -42,7 +46,8 @@ class FeedViewModel @Inject constructor(
|
|||||||
private val repository: TrackingRepository,
|
private val repository: TrackingRepository,
|
||||||
private val scheduler: TrackWorker.Scheduler,
|
private val scheduler: TrackWorker.Scheduler,
|
||||||
private val mangaListMapper: MangaListMapper,
|
private val mangaListMapper: MangaListMapper,
|
||||||
) : BaseViewModel() {
|
private val quickFilter: UpdatesListQuickFilter,
|
||||||
|
) : BaseViewModel(), QuickFilterListener by quickFilter {
|
||||||
|
|
||||||
private val limit = MutableStateFlow(PAGE_SIZE)
|
private val limit = MutableStateFlow(PAGE_SIZE)
|
||||||
private val isReady = AtomicBoolean(false)
|
private val isReady = AtomicBoolean(false)
|
||||||
@@ -57,11 +62,16 @@ class FeedViewModel @Inject constructor(
|
|||||||
)
|
)
|
||||||
|
|
||||||
val onFeedCleared = MutableEventFlow<Unit>()
|
val onFeedCleared = MutableEventFlow<Unit>()
|
||||||
|
|
||||||
|
@Suppress("USELESS_CAST")
|
||||||
val content = combine(
|
val content = combine(
|
||||||
observeHeader(),
|
observeHeader(),
|
||||||
repository.observeTrackingLog(limit),
|
quickFilter.appliedOptions,
|
||||||
) { header, list ->
|
combine(limit, quickFilter.appliedOptions, ::Pair)
|
||||||
val result = ArrayList<ListModel>((list.size * 1.4).toInt().coerceAtLeast(2))
|
.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) {
|
if (header != null) {
|
||||||
result += header
|
result += header
|
||||||
}
|
}
|
||||||
@@ -76,7 +86,9 @@ class FeedViewModel @Inject constructor(
|
|||||||
isReady.set(true)
|
isReady.set(true)
|
||||||
list.mapListTo(result)
|
list.mapListTo(result)
|
||||||
}
|
}
|
||||||
result
|
result as List<ListModel>
|
||||||
|
}.catch { e ->
|
||||||
|
emit(listOf(e.toErrorState(canRetry = false)))
|
||||||
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, listOf(LoadingState))
|
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, listOf(LoadingState))
|
||||||
|
|
||||||
init {
|
init {
|
||||||
@@ -129,7 +141,9 @@ class FeedViewModel @Inject constructor(
|
|||||||
|
|
||||||
private fun observeHeader() = isHeaderEnabled.flatMapLatest { hasHeader ->
|
private fun observeHeader() = isHeaderEnabled.flatMapLatest { hasHeader ->
|
||||||
if (hasHeader) {
|
if (hasHeader) {
|
||||||
repository.observeUpdatedManga(10).map { mangaList ->
|
quickFilter.appliedOptions.flatMapLatest {
|
||||||
|
repository.observeUpdatedManga(10, it)
|
||||||
|
}.map { mangaList ->
|
||||||
if (mangaList.isEmpty()) {
|
if (mangaList.isEmpty()) {
|
||||||
null
|
null
|
||||||
} else {
|
} 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.listHeaderAD
|
||||||
import org.koitharu.kotatsu.list.ui.adapter.loadingFooterAD
|
import org.koitharu.kotatsu.list.ui.adapter.loadingFooterAD
|
||||||
import org.koitharu.kotatsu.list.ui.adapter.loadingStateAD
|
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.model.ListModel
|
||||||
import org.koitharu.kotatsu.list.ui.size.ItemSizeResolver
|
import org.koitharu.kotatsu.list.ui.size.ItemSizeResolver
|
||||||
import org.koitharu.kotatsu.tracker.ui.feed.model.FeedItem
|
import org.koitharu.kotatsu.tracker.ui.feed.model.FeedItem
|
||||||
@@ -44,6 +45,7 @@ class FeedAdapter(
|
|||||||
addDelegate(ListItemType.STATE_ERROR, errorStateListAD(listener))
|
addDelegate(ListItemType.STATE_ERROR, errorStateListAD(listener))
|
||||||
addDelegate(ListItemType.HEADER, listHeaderAD(listener))
|
addDelegate(ListItemType.HEADER, listHeaderAD(listener))
|
||||||
addDelegate(ListItemType.STATE_EMPTY, emptyStateListAD(coil, lifecycleOwner, listener))
|
addDelegate(ListItemType.STATE_EMPTY, emptyStateListAD(coil, lifecycleOwner, listener))
|
||||||
|
addDelegate(ListItemType.QUICK_FILTER, quickFilterAD(listener))
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getSectionText(context: Context, position: Int): CharSequence? {
|
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.SharingStarted
|
||||||
import kotlinx.coroutines.flow.catch
|
import kotlinx.coroutines.flow.catch
|
||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
|
import kotlinx.coroutines.flow.flatMapLatest
|
||||||
import kotlinx.coroutines.flow.onStart
|
import kotlinx.coroutines.flow.onStart
|
||||||
import kotlinx.coroutines.flow.stateIn
|
import kotlinx.coroutines.flow.stateIn
|
||||||
import kotlinx.coroutines.plus
|
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.calculateTimeAgo
|
||||||
import org.koitharu.kotatsu.core.util.ext.onFirst
|
import org.koitharu.kotatsu.core.util.ext.onFirst
|
||||||
import org.koitharu.kotatsu.download.ui.worker.DownloadWorker
|
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.MangaListMapper
|
||||||
|
import org.koitharu.kotatsu.list.domain.QuickFilterListener
|
||||||
import org.koitharu.kotatsu.list.ui.MangaListViewModel
|
import org.koitharu.kotatsu.list.ui.MangaListViewModel
|
||||||
import org.koitharu.kotatsu.list.ui.model.EmptyState
|
import org.koitharu.kotatsu.list.ui.model.EmptyState
|
||||||
import org.koitharu.kotatsu.list.ui.model.ListHeader
|
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.LoadingState
|
||||||
import org.koitharu.kotatsu.list.ui.model.toErrorState
|
import org.koitharu.kotatsu.list.ui.model.toErrorState
|
||||||
import org.koitharu.kotatsu.tracker.domain.TrackingRepository
|
import org.koitharu.kotatsu.tracker.domain.TrackingRepository
|
||||||
|
import org.koitharu.kotatsu.tracker.domain.UpdatesListQuickFilter
|
||||||
import org.koitharu.kotatsu.tracker.domain.model.MangaTracking
|
import org.koitharu.kotatsu.tracker.domain.model.MangaTracking
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@@ -33,16 +37,24 @@ class UpdatesViewModel @Inject constructor(
|
|||||||
private val repository: TrackingRepository,
|
private val repository: TrackingRepository,
|
||||||
settings: AppSettings,
|
settings: AppSettings,
|
||||||
private val mangaListMapper: MangaListMapper,
|
private val mangaListMapper: MangaListMapper,
|
||||||
|
private val quickFilter: UpdatesListQuickFilter,
|
||||||
downloadScheduler: DownloadWorker.Scheduler,
|
downloadScheduler: DownloadWorker.Scheduler,
|
||||||
) : MangaListViewModel(settings, downloadScheduler) {
|
) : MangaListViewModel(settings, downloadScheduler), QuickFilterListener by quickFilter {
|
||||||
|
|
||||||
override val content = combine(
|
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 },
|
settings.observeAsFlow(AppSettings.KEY_UPDATED_GROUPING) { isUpdatedGroupingEnabled },
|
||||||
observeListModeWithTriggers(),
|
observeListModeWithTriggers(),
|
||||||
) { mangaList, grouping, mode ->
|
) { mangaList, filters, grouping, mode ->
|
||||||
when {
|
when {
|
||||||
mangaList.isEmpty() -> listOf(
|
mangaList.isEmpty() -> listOfNotNull(
|
||||||
|
quickFilter.filterItem(filters),
|
||||||
EmptyState(
|
EmptyState(
|
||||||
icon = R.drawable.ic_empty_history,
|
icon = R.drawable.ic_empty_history,
|
||||||
textPrimary = R.string.text_history_holder_primary,
|
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 {
|
}.onStart {
|
||||||
loadingCounter.increment()
|
loadingCounter.increment()
|
||||||
@@ -77,8 +89,13 @@ class UpdatesViewModel @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun List<MangaTracking>.toUi(mode: ListMode, grouped: Boolean): List<ListModel> {
|
private suspend fun List<MangaTracking>.toUi(
|
||||||
val result = ArrayList<ListModel>(if (grouped) (size * 1.4).toInt() else size)
|
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
|
var prevHeader: DateTimeAgo? = null
|
||||||
for (item in this) {
|
for (item in this) {
|
||||||
if (grouped) {
|
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="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="connection_ok">Connection is OK</string>
|
||||||
<string name="invalid_proxy_configuration">Invalid proxy configuration</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>
|
</resources>
|
||||||
|
|||||||
@@ -38,6 +38,12 @@
|
|||||||
android:valueTo="150"
|
android:valueTo="150"
|
||||||
app:defaultValue="100" />
|
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
|
<ListPreference
|
||||||
android:entries="@array/progress_indicators"
|
android:entries="@array/progress_indicators"
|
||||||
android:key="progress_indicators"
|
android:key="progress_indicators"
|
||||||
|
|||||||
Reference in New Issue
Block a user