Quick filter for favorites

master
Koitharu 2 years ago
parent 8b71f99666
commit 00396f2e1b
Signed by: Koitharu
GPG Key ID: 676DEE768C17A9D7

@ -17,6 +17,9 @@ class NetworkState(
private val callback = NetworkCallbackImpl() private val callback = NetworkCallbackImpl()
override val value: Boolean
get() = connectivityManager.isOnline(settings)
@Synchronized @Synchronized
override fun onActive() { override fun onActive() {
invalidate() invalidate()

@ -13,7 +13,7 @@ abstract class MediatorStateFlow<T>(initialValue: T) : StateFlow<T> {
final override val replayCache: List<T> final override val replayCache: List<T>
get() = delegate.replayCache get() = delegate.replayCache
final override val value: T override val value: T
get() = delegate.value get() = delegate.value
final override suspend fun collect(collector: FlowCollector<T>): Nothing { final override suspend fun collect(collector: FlowCollector<T>): Nothing {

@ -11,7 +11,9 @@ import androidx.sqlite.db.SimpleSQLiteQuery
import androidx.sqlite.db.SupportSQLiteQuery import androidx.sqlite.db.SupportSQLiteQuery
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import org.intellij.lang.annotations.Language import org.intellij.lang.annotations.Language
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.ListSortOrder import org.koitharu.kotatsu.list.domain.ListSortOrder
@Dao @Dao
@ -27,21 +29,11 @@ abstract class FavouritesDao {
@Query("SELECT * FROM favourites WHERE deleted_at = 0 GROUP BY manga_id ORDER BY created_at DESC LIMIT :limit") @Query("SELECT * FROM favourites WHERE deleted_at = 0 GROUP BY manga_id ORDER BY created_at DESC LIMIT :limit")
abstract suspend fun findLast(limit: Int): List<FavouriteManga> abstract suspend fun findLast(limit: Int): List<FavouriteManga>
fun observeAll(order: ListSortOrder, limit: Int): Flow<List<FavouriteManga>> { fun observeAll(
val orderBy = getOrderBy(order) order: ListSortOrder,
val query = buildString { filterOptions: Set<ListFilterOption>,
append( limit: Int
"SELECT * FROM favourites LEFT JOIN manga ON favourites.manga_id = manga.manga_id " + ): Flow<List<FavouriteManga>> = observeAll(0L, order, filterOptions, limit)
"WHERE favourites.deleted_at = 0 GROUP BY favourites.manga_id ORDER BY ",
)
append(orderBy)
if (limit > 0) {
append(" LIMIT ")
append(limit)
}
}
return observeAllImpl(SimpleSQLiteQuery(query))
}
@Transaction @Transaction
@Query("SELECT * FROM favourites WHERE deleted_at = 0 ORDER BY created_at DESC LIMIT :limit OFFSET :offset") @Query("SELECT * FROM favourites WHERE deleted_at = 0 ORDER BY created_at DESC LIMIT :limit OFFSET :offset")
@ -57,13 +49,37 @@ abstract class FavouritesDao {
) )
abstract suspend fun findAll(categoryId: Long): List<FavouriteManga> abstract suspend fun findAll(categoryId: Long): List<FavouriteManga>
fun observeAll(categoryId: Long, order: ListSortOrder, limit: Int): Flow<List<FavouriteManga>> { fun observeAll(
categoryId: Long,
order: ListSortOrder,
filterOptions: Set<ListFilterOption>,
limit: Int
): Flow<List<FavouriteManga>> {
val orderBy = getOrderBy(order) val orderBy = getOrderBy(order)
val query = buildString { val query = buildString {
append( append(
"SELECT * FROM favourites LEFT JOIN manga ON favourites.manga_id = manga.manga_id " + "SELECT * FROM favourites LEFT JOIN manga ON favourites.manga_id = manga.manga_id " +
"WHERE category_id = ? AND deleted_at = 0 GROUP BY favourites.manga_id ORDER BY ", "WHERE deleted_at = 0",
) )
if (categoryId != 0L) {
append(" AND category_id = ")
append(categoryId)
}
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(" GROUP BY favourites.manga_id ORDER BY ")
append(orderBy) append(orderBy)
if (limit > 0) { if (limit > 0) {
append(" LIMIT ") append(" LIMIT ")
@ -71,7 +87,7 @@ abstract class FavouritesDao {
} }
} }
return observeAllImpl(SimpleSQLiteQuery(query, arrayOf<Any>(categoryId))) return observeAllImpl(SimpleSQLiteQuery(query))
} }
suspend fun findCovers(categoryId: Long, order: ListSortOrder): List<Cover> { suspend fun findCovers(categoryId: Long, order: ListSortOrder): List<Cover> {
@ -191,4 +207,11 @@ abstract class FavouritesDao {
else -> throw IllegalArgumentException("Sort order $sortOrder is not supported") else -> throw IllegalArgumentException("Sort order $sortOrder is not supported")
} }
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.NEW_CHAPTERS -> "(SELECT chapters_new FROM tracks WHERE tracks.manga_id = favourites.manga_id) > 0"
is ListFilterOption.Tag -> "EXISTS(SELECT * FROM manga_tags WHERE favourites.manga_id = manga_tags.manga_id AND tag_id = ${tag.toEntity().id})"
else -> throw IllegalArgumentException("Unsupported option $this")
}
} }

@ -0,0 +1,26 @@
package org.koitharu.kotatsu.favourites.domain
import org.koitharu.kotatsu.core.os.NetworkState
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.list.domain.ListFilterOption
import org.koitharu.kotatsu.list.domain.MangaListQuickFilter
import javax.inject.Inject
class FavoritesListQuickFilter @Inject constructor(
private val settings: AppSettings,
private val repository: FavouritesRepository,
networkState: NetworkState,
) : MangaListQuickFilter() {
init {
setFilterOption(ListFilterOption.Downloaded, !networkState.value)
}
override suspend fun getAvailableFilterOptions(): List<ListFilterOption> = buildList {
add(ListFilterOption.Downloaded)
if (settings.isTrackerEnabled) {
add(ListFilterOption.Macro.NEW_CHAPTERS)
}
add(ListFilterOption.Macro.COMPLETED)
}
}

@ -19,6 +19,7 @@ import org.koitharu.kotatsu.favourites.data.toFavouriteCategory
import org.koitharu.kotatsu.favourites.data.toManga import org.koitharu.kotatsu.favourites.data.toManga
import org.koitharu.kotatsu.favourites.data.toMangaList import org.koitharu.kotatsu.favourites.data.toMangaList
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.ListSortOrder import org.koitharu.kotatsu.list.domain.ListSortOrder
import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.Manga
import javax.inject.Inject import javax.inject.Inject
@ -38,8 +39,8 @@ class FavouritesRepository @Inject constructor(
return entities.toMangaList() return entities.toMangaList()
} }
fun observeAll(order: ListSortOrder, limit: Int): Flow<List<Manga>> { fun observeAll(order: ListSortOrder, filterOptions: Set<ListFilterOption>, limit: Int): Flow<List<Manga>> {
return db.getFavouritesDao().observeAll(order, limit) return db.getFavouritesDao().observeAll(order, filterOptions, limit)
.mapItems { it.toManga() } .mapItems { it.toManga() }
} }
@ -48,14 +49,19 @@ class FavouritesRepository @Inject constructor(
return entities.toMangaList() return entities.toMangaList()
} }
fun observeAll(categoryId: Long, order: ListSortOrder, limit: Int): Flow<List<Manga>> { fun observeAll(
return db.getFavouritesDao().observeAll(categoryId, order, limit) categoryId: Long,
order: ListSortOrder,
filterOptions: Set<ListFilterOption>,
limit: Int
): Flow<List<Manga>> {
return db.getFavouritesDao().observeAll(categoryId, order, filterOptions, limit)
.mapItems { it.toManga() } .mapItems { it.toManga() }
} }
fun observeAll(categoryId: Long, limit: Int): Flow<List<Manga>> { fun observeAll(categoryId: Long, filterOptions: Set<ListFilterOption>, limit: Int): Flow<List<Manga>> {
return observeOrder(categoryId) return observeOrder(categoryId)
.flatMapLatest { order -> observeAll(categoryId, order, limit) } .flatMapLatest { order -> observeAll(categoryId, order, filterOptions, limit) }
} }
fun observeMangaCount(): Flow<Int> { fun observeMangaCount(): Flow<Int> {

@ -35,6 +35,8 @@ class FavouritesListFragment : MangaListFragment(), PopupMenu.OnMenuItemClickLis
override fun onScrolledToEnd() = viewModel.requestMoreItems() override fun onScrolledToEnd() = viewModel.requestMoreItems()
override fun onEmptyActionClick() = viewModel.clearFilter()
override fun onFilterClick(view: View?) { override fun onFilterClick(view: View?) {
val menu = PopupMenu(view?.context ?: return, view) val menu = PopupMenu(view?.context ?: return, view)
menu.setOnMenuItemClickListener(this) menu.setOnMenuItemClickListener(this)

@ -4,6 +4,9 @@ import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
@ -15,21 +18,28 @@ import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.plus import kotlinx.coroutines.plus
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.model.isLocal
import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.prefs.ListMode
import org.koitharu.kotatsu.core.prefs.observeAsFlow import org.koitharu.kotatsu.core.prefs.observeAsFlow
import org.koitharu.kotatsu.core.ui.util.ReversibleAction import org.koitharu.kotatsu.core.ui.util.ReversibleAction
import org.koitharu.kotatsu.core.util.ext.call import org.koitharu.kotatsu.core.util.ext.call
import org.koitharu.kotatsu.download.ui.worker.DownloadWorker import org.koitharu.kotatsu.download.ui.worker.DownloadWorker
import org.koitharu.kotatsu.favourites.domain.FavoritesListQuickFilter
import org.koitharu.kotatsu.favourites.domain.FavouritesRepository import org.koitharu.kotatsu.favourites.domain.FavouritesRepository
import org.koitharu.kotatsu.favourites.ui.list.FavouritesListFragment.Companion.ARG_CATEGORY_ID import org.koitharu.kotatsu.favourites.ui.list.FavouritesListFragment.Companion.ARG_CATEGORY_ID
import org.koitharu.kotatsu.favourites.ui.list.FavouritesListFragment.Companion.NO_ID import org.koitharu.kotatsu.favourites.ui.list.FavouritesListFragment.Companion.NO_ID
import org.koitharu.kotatsu.history.domain.MarkAsReadUseCase import org.koitharu.kotatsu.history.domain.MarkAsReadUseCase
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.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.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.local.data.LocalMangaRepository
import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.Manga
import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicBoolean
import javax.inject.Inject import javax.inject.Inject
@ -42,9 +52,11 @@ class FavouritesListViewModel @Inject constructor(
private val repository: FavouritesRepository, private val repository: FavouritesRepository,
private val mangaListMapper: MangaListMapper, private val mangaListMapper: MangaListMapper,
private val markAsReadUseCase: MarkAsReadUseCase, private val markAsReadUseCase: MarkAsReadUseCase,
private val quickFilter: FavoritesListQuickFilter,
private val localMangaRepository: LocalMangaRepository,
settings: AppSettings, settings: AppSettings,
downloadScheduler: DownloadWorker.Scheduler, downloadScheduler: DownloadWorker.Scheduler,
) : MangaListViewModel(settings, downloadScheduler) { ) : MangaListViewModel(settings, downloadScheduler), QuickFilterListener by quickFilter {
val categoryId: Long = savedStateHandle[ARG_CATEGORY_ID] ?: NO_ID val categoryId: Long = savedStateHandle[ARG_CATEGORY_ID] ?: NO_ID
private val refreshTrigger = MutableStateFlow(Any()) private val refreshTrigger = MutableStateFlow(Any())
@ -66,26 +78,19 @@ class FavouritesListViewModel @Inject constructor(
override val content = combine( override val content = combine(
observeFavorites(), observeFavorites(),
quickFilter.appliedOptions,
observeListModeWithTriggers(), observeListModeWithTriggers(),
refreshTrigger, refreshTrigger,
) { list, mode, _ -> ) { list, filters, mode, _ ->
when { when {
list.isEmpty() -> listOf( list.isEmpty() -> if (filters.isEmpty()) {
EmptyState( listOf(getEmptyState(hasFilters = false))
icon = R.drawable.ic_empty_favourites,
textPrimary = R.string.text_empty_holder_primary,
textSecondary = if (categoryId == NO_ID) {
R.string.you_have_not_favourites_yet
} else { } else {
R.string.favourites_category_empty listOf(quickFilter.filterItem(filters), getEmptyState(hasFilters = true))
}, }
actionStringRes = 0,
),
)
else -> { else -> {
isReady.set(true) list.mapList(mode, filters).also { isReady.set(true) }
mangaListMapper.toListModelList(list, mode)
} }
} }
}.catch { }.catch {
@ -134,12 +139,55 @@ class FavouritesListViewModel @Inject constructor(
} }
} }
private suspend fun List<Manga>.mapList(mode: ListMode, filters: Set<ListFilterOption>): List<ListModel> {
val list = if (ListFilterOption.Downloaded in filters) {
mapToLocal()
} else {
this
}
val result = ArrayList<ListModel>(list.size + 1)
result += quickFilter.filterItem(filters)
mangaListMapper.toListModelList(result, list, mode)
return result
}
private fun observeFavorites() = if (categoryId == NO_ID) { private fun observeFavorites() = if (categoryId == NO_ID) {
combine(sortOrder.filterNotNull(), limit, ::Pair) combine(sortOrder.filterNotNull(), quickFilter.appliedOptions, limit, ::Triple)
.flatMapLatest { repository.observeAll(it.first, it.second) } .flatMapLatest { repository.observeAll(it.first, it.second - ListFilterOption.Downloaded, it.third) }
} else {
combine(quickFilter.appliedOptions, limit, ::Pair)
.flatMapLatest { repository.observeAll(categoryId, it.first - ListFilterOption.Downloaded, it.second) }
}
private fun getEmptyState(hasFilters: Boolean) = if (hasFilters) {
EmptyState(
icon = R.drawable.ic_empty_favourites,
textPrimary = R.string.nothing_found,
textSecondary = R.string.text_empty_holder_secondary_filtered,
actionStringRes = R.string.reset_filter,
)
} else {
EmptyState(
icon = R.drawable.ic_empty_favourites,
textPrimary = R.string.text_empty_holder_primary,
textSecondary = if (categoryId == NO_ID) {
R.string.you_have_not_favourites_yet
} else { } else {
limit.flatMapLatest { R.string.favourites_category_empty
repository.observeAll(categoryId, it) },
actionStringRes = 0,
)
}
private suspend fun List<Manga>.mapToLocal(): List<Manga> = coroutineScope {
map {
async {
if (it.isLocal) {
it
} else {
localMangaRepository.findSavedManga(it)?.manga
}
} }
}.awaitAll().filterNotNull()
} }
} }

@ -1,5 +1,6 @@
package org.koitharu.kotatsu.history.domain package org.koitharu.kotatsu.history.domain
import org.koitharu.kotatsu.core.os.NetworkState
import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.history.data.HistoryRepository import org.koitharu.kotatsu.history.data.HistoryRepository
import org.koitharu.kotatsu.list.domain.ListFilterOption import org.koitharu.kotatsu.list.domain.ListFilterOption
@ -9,8 +10,13 @@ import javax.inject.Inject
class HistoryListQuickFilter @Inject constructor( class HistoryListQuickFilter @Inject constructor(
private val settings: AppSettings, private val settings: AppSettings,
private val repository: HistoryRepository, private val repository: HistoryRepository,
networkState: NetworkState,
) : MangaListQuickFilter() { ) : MangaListQuickFilter() {
init {
setFilterOption(ListFilterOption.Downloaded, !networkState.value)
}
override suspend fun getAvailableFilterOptions(): List<ListFilterOption> = buildList { override suspend fun getAvailableFilterOptions(): List<ListFilterOption> = buildList {
add(ListFilterOption.Downloaded) add(ListFilterOption.Downloaded)
if (settings.isTrackerEnabled) { if (settings.isTrackerEnabled) {

@ -9,7 +9,6 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.model.isLocal import org.koitharu.kotatsu.core.model.isLocal
import org.koitharu.kotatsu.core.os.NetworkManageIntent
import org.koitharu.kotatsu.core.ui.list.ListSelectionController import org.koitharu.kotatsu.core.ui.list.ListSelectionController
import org.koitharu.kotatsu.core.ui.list.RecyclerScrollKeeper import org.koitharu.kotatsu.core.ui.list.RecyclerScrollKeeper
import org.koitharu.kotatsu.core.ui.util.MenuInvalidator import org.koitharu.kotatsu.core.ui.util.MenuInvalidator
@ -34,9 +33,7 @@ class HistoryListFragment : MangaListFragment() {
override fun onScrolledToEnd() = viewModel.requestMoreItems() override fun onScrolledToEnd() = viewModel.requestMoreItems()
override fun onEmptyActionClick() { override fun onEmptyActionClick() = viewModel.clearFilter()
startActivity(NetworkManageIntent())
}
override fun onCreateActionMode(controller: ListSelectionController, mode: ActionMode, menu: Menu): Boolean { override fun onCreateActionMode(controller: ListSelectionController, mode: ActionMode, menu: Menu): Boolean {
mode.menuInflater.inflate(R.menu.mode_history, menu) mode.menuInflater.inflate(R.menu.mode_history, menu)

@ -18,7 +18,6 @@ import kotlinx.coroutines.plus
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.model.MangaHistory import org.koitharu.kotatsu.core.model.MangaHistory
import org.koitharu.kotatsu.core.model.isLocal import org.koitharu.kotatsu.core.model.isLocal
import org.koitharu.kotatsu.core.os.NetworkState
import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.prefs.ListMode import org.koitharu.kotatsu.core.prefs.ListMode
import org.koitharu.kotatsu.core.prefs.observeAsFlow import org.koitharu.kotatsu.core.prefs.observeAsFlow
@ -26,7 +25,6 @@ import org.koitharu.kotatsu.core.prefs.observeAsStateFlow
import org.koitharu.kotatsu.core.ui.util.ReversibleAction import org.koitharu.kotatsu.core.ui.util.ReversibleAction
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.core.util.ext.combine
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.history.data.HistoryRepository import org.koitharu.kotatsu.history.data.HistoryRepository
@ -38,7 +36,6 @@ import org.koitharu.kotatsu.list.domain.ListSortOrder
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.domain.QuickFilterListener
import org.koitharu.kotatsu.list.ui.MangaListViewModel import org.koitharu.kotatsu.list.ui.MangaListViewModel
import org.koitharu.kotatsu.list.ui.model.EmptyHint
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
@ -61,7 +58,6 @@ class HistoryListViewModel @Inject constructor(
private val localMangaRepository: LocalMangaRepository, private val localMangaRepository: LocalMangaRepository,
private val markAsReadUseCase: MarkAsReadUseCase, private val markAsReadUseCase: MarkAsReadUseCase,
private val quickFilter: HistoryListQuickFilter, private val quickFilter: HistoryListQuickFilter,
networkState: NetworkState,
downloadScheduler: DownloadWorker.Scheduler, downloadScheduler: DownloadWorker.Scheduler,
) : MangaListViewModel(settings, downloadScheduler), QuickFilterListener by quickFilter { ) : MangaListViewModel(settings, downloadScheduler), QuickFilterListener by quickFilter {
@ -98,9 +94,8 @@ class HistoryListViewModel @Inject constructor(
observeHistory(), observeHistory(),
isGroupingEnabled, isGroupingEnabled,
observeListModeWithTriggers(), observeListModeWithTriggers(),
networkState,
settings.observeAsFlow(AppSettings.KEY_INCOGNITO_MODE) { isIncognitoModeEnabled }, settings.observeAsFlow(AppSettings.KEY_INCOGNITO_MODE) { isIncognitoModeEnabled },
) { filters, list, grouped, mode, online, incognito -> ) { filters, list, grouped, mode, incognito ->
when { when {
list.isEmpty() -> { list.isEmpty() -> {
if (filters.isEmpty()) { if (filters.isEmpty()) {
@ -112,7 +107,7 @@ class HistoryListViewModel @Inject constructor(
else -> { else -> {
isReady.set(true) isReady.set(true)
mapList(filters, list, grouped, mode, online, incognito) mapList(list, grouped, mode, filters, incognito)
} }
} }
}.onStart { }.onStart {
@ -166,19 +161,18 @@ class HistoryListViewModel @Inject constructor(
.flatMapLatest { repository.observeAllWithHistory(it.first, it.second - ListFilterOption.Downloaded, it.third) } .flatMapLatest { repository.observeAllWithHistory(it.first, it.second - ListFilterOption.Downloaded, it.third) }
private suspend fun mapList( private suspend fun mapList(
filters: Set<ListFilterOption>,
historyList: List<MangaWithHistory>, historyList: List<MangaWithHistory>,
grouped: Boolean, grouped: Boolean,
mode: ListMode, mode: ListMode,
isOnline: Boolean, filters: Set<ListFilterOption>,
isIncognito: Boolean, isIncognito: Boolean,
): List<ListModel> { ): List<ListModel> {
val list = if (!isOnline || ListFilterOption.Downloaded in filters) { val list = if (ListFilterOption.Downloaded in filters) {
historyList.mapToLocal() historyList.mapToLocal()
} else { } else {
historyList historyList
} }
val result = ArrayList<ListModel>((if (grouped) (list.size * 1.4).toInt() else list.size) + 3) val result = ArrayList<ListModel>((if (grouped) (list.size * 1.4).toInt() else list.size) + 2)
result += quickFilter.filterItem(filters) result += quickFilter.filterItem(filters)
if (isIncognito) { if (isIncognito) {
result += TipModel( result += TipModel(
@ -192,14 +186,6 @@ class HistoryListViewModel @Inject constructor(
} }
val order = sortOrder.value val order = sortOrder.value
var prevHeader: ListHeader? = null var prevHeader: ListHeader? = null
if (!isOnline) {
result += EmptyHint(
icon = R.drawable.ic_empty_common,
textPrimary = R.string.network_unavailable,
textSecondary = R.string.network_unavailable_hint,
actionStringRes = R.string.manage,
)
}
var isEmpty = true var isEmpty = true
for ((manga, history) in list) { for ((manga, history) in list) {
isEmpty = false isEmpty = false
@ -263,8 +249,8 @@ class HistoryListViewModel @Inject constructor(
EmptyState( EmptyState(
icon = R.drawable.ic_empty_history, icon = R.drawable.ic_empty_history,
textPrimary = R.string.nothing_found, textPrimary = R.string.nothing_found,
textSecondary = R.string.text_history_holder_secondary_filtered, textSecondary = R.string.text_empty_holder_secondary_filtered,
actionStringRes = 0, actionStringRes = R.string.reset_filter,
) )
} else { } else {
EmptyState( EmptyState(

@ -17,6 +17,16 @@ abstract class MangaListQuickFilter : QuickFilterListener {
val appliedOptions val appliedOptions
get() = appliedFilter.asStateFlow() get() = appliedFilter.asStateFlow()
override fun setFilterOption(option: ListFilterOption, isApplied: Boolean) {
appliedFilter.value = ArraySet(appliedFilter.value).also {
if (isApplied) {
it.add(option)
} else {
it.remove(option)
}
}
}
override fun toggleFilterOption(option: ListFilterOption) { override fun toggleFilterOption(option: ListFilterOption) {
appliedFilter.value = ArraySet(appliedFilter.value).also { appliedFilter.value = ArraySet(appliedFilter.value).also {
if (option in it) { if (option in it) {

@ -2,6 +2,8 @@ package org.koitharu.kotatsu.list.domain
interface QuickFilterListener { interface QuickFilterListener {
fun setFilterOption(option: ListFilterOption, isApplied: Boolean)
fun toggleFilterOption(option: ListFilterOption) fun toggleFilterOption(option: ListFilterOption)
fun clearFilter() fun clearFilter()

@ -17,3 +17,12 @@ fun Throwable.toErrorState(canRetry: Boolean = true, @StringRes secondaryAction:
fun Throwable.toErrorFooter() = ErrorFooter( fun Throwable.toErrorFooter() = ErrorFooter(
exception = this, exception = this,
) )
operator fun ListModel.plus(list: List<ListModel>): List<ListModel> {
val result = ArrayList<ListModel>(list.size + 1)
result.add(this)
result.addAll(list)
return result
}
operator fun ListModel.plus(other: ListModel): List<ListModel> = listOf(this, other)

@ -5,6 +5,7 @@
android:id="@+id/scrollView" android:id="@+id/scrollView"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:clipChildren="false"
android:clipToPadding="false" android:clipToPadding="false"
android:paddingHorizontal="@dimen/list_spacing" android:paddingHorizontal="@dimen/list_spacing"
android:scrollbars="none"> android:scrollbars="none">
@ -15,7 +16,8 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:clipChildren="false" android:clipChildren="false"
android:clipToPadding="false" android:clipToPadding="false"
android:paddingVertical="2dp" android:paddingTop="2dp"
android:paddingBottom="6dp"
app:chipStyle="@style/Widget.Kotatsu.Chip.Filter" app:chipStyle="@style/Widget.Kotatsu.Chip.Filter"
app:selectionRequired="false" app:selectionRequired="false"
app:singleLine="true" app:singleLine="true"

@ -96,7 +96,7 @@
<string name="text_search_holder_secondary">Try to reformulate the query.</string> <string name="text_search_holder_secondary">Try to reformulate the query.</string>
<string name="text_history_holder_primary">What you read will be displayed here</string> <string name="text_history_holder_primary">What you read will be displayed here</string>
<string name="text_history_holder_secondary">Find what to read in the «Explore» section</string> <string name="text_history_holder_secondary">Find what to read in the «Explore» section</string>
<string name="text_history_holder_secondary_filtered">There are no manga matching the filters you selected</string> <string name="text_empty_holder_secondary_filtered">There are no manga matching the filters you selected</string>
<string name="text_local_holder_primary">Save something first</string> <string name="text_local_holder_primary">Save something first</string>
<string name="text_local_holder_secondary">Save something from an online catalog or import it from a file.</string> <string name="text_local_holder_secondary">Save something from an online catalog or import it from a file.</string>
<string name="manga_shelf">Shelf</string> <string name="manga_shelf">Shelf</string>

Loading…
Cancel
Save