Refactor quick filter implementation
This commit is contained in:
1
.idea/gradle.xml
generated
1
.idea/gradle.xml
generated
@@ -4,6 +4,7 @@
|
|||||||
<component name="GradleSettings">
|
<component name="GradleSettings">
|
||||||
<option name="linkedExternalProjectsSettings">
|
<option name="linkedExternalProjectsSettings">
|
||||||
<GradleProjectSettings>
|
<GradleProjectSettings>
|
||||||
|
<option name="testRunner" value="CHOOSE_PER_TEST" />
|
||||||
<option name="externalProjectPath" value="$PROJECT_DIR$" />
|
<option name="externalProjectPath" value="$PROJECT_DIR$" />
|
||||||
<option name="gradleJvm" value="#GRADLE_LOCAL_JAVA_HOME" />
|
<option name="gradleJvm" value="#GRADLE_LOCAL_JAVA_HOME" />
|
||||||
<option name="modules">
|
<option name="modules">
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ 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.koitharu.kotatsu.core.db.entity.TagEntity
|
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.ListFilterOption
|
||||||
import org.koitharu.kotatsu.list.domain.ListSortOrder
|
import org.koitharu.kotatsu.list.domain.ListSortOrder
|
||||||
|
|
||||||
@@ -51,9 +52,19 @@ abstract class HistoryDao {
|
|||||||
"SELECT * FROM history LEFT JOIN manga ON history.manga_id = manga.manga_id " +
|
"SELECT * FROM history LEFT JOIN manga ON history.manga_id = manga.manga_id " +
|
||||||
"WHERE history.deleted_at = 0",
|
"WHERE history.deleted_at = 0",
|
||||||
)
|
)
|
||||||
for (option in filterOptions) {
|
val groupedOptions = filterOptions.groupBy { it.groupKey }
|
||||||
|
for ((_, group) in groupedOptions) {
|
||||||
|
if (group.isEmpty()) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
append(" AND ")
|
append(" AND ")
|
||||||
append(option.getCondition())
|
if (group.size > 1) {
|
||||||
|
group.joinTo(this, separator = " OR ", prefix = "(", postfix = ")") {
|
||||||
|
it.getCondition()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
append(group.single().getCondition())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
append(" GROUP BY history.manga_id ORDER BY ")
|
append(" GROUP BY history.manga_id ORDER BY ")
|
||||||
append(orderBy)
|
append(orderBy)
|
||||||
@@ -159,9 +170,11 @@ abstract class HistoryDao {
|
|||||||
protected abstract fun observeAllImpl(query: SupportSQLiteQuery): Flow<List<HistoryWithManga>>
|
protected abstract fun observeAllImpl(query: SupportSQLiteQuery): Flow<List<HistoryWithManga>>
|
||||||
|
|
||||||
private fun ListFilterOption.getCondition(): String = when (this) {
|
private fun ListFilterOption.getCondition(): String = when (this) {
|
||||||
ListFilterOption.NEW_CHAPTERS -> "(SELECT chapters_new FROM tracks WHERE tracks.manga_id = history.manga_id) > 0"
|
ListFilterOption.Downloaded -> throw IllegalArgumentException("Unsupported option $this")
|
||||||
ListFilterOption.FAVORITE -> "EXISTS(SELECT * FROM favourites WHERE history.manga_id = favourites.manga_id)"
|
is ListFilterOption.Favorite -> "EXISTS(SELECT * FROM favourites WHERE history.manga_id = favourites.manga_id AND category_id = ${category.id})"
|
||||||
ListFilterOption.COMPLETED -> "percent >= 0.9999"
|
ListFilterOption.Macro.COMPLETED -> "percent >= 0.9999"
|
||||||
ListFilterOption.DOWNLOADED -> throw IllegalArgumentException("Unsupported option $this")
|
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)"
|
||||||
|
is ListFilterOption.Tag -> "EXISTS(SELECT * FROM manga_tags WHERE history.manga_id = manga_tags.manga_id AND tag_id = ${tag.toEntity().id})"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package org.koitharu.kotatsu.history.domain
|
||||||
|
|
||||||
|
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||||
|
import org.koitharu.kotatsu.history.data.HistoryRepository
|
||||||
|
import org.koitharu.kotatsu.list.domain.ListFilterOption
|
||||||
|
import org.koitharu.kotatsu.list.domain.MangaListQuickFilter
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
class HistoryListQuickFilter @Inject constructor(
|
||||||
|
private val settings: AppSettings,
|
||||||
|
private val repository: HistoryRepository,
|
||||||
|
) : MangaListQuickFilter() {
|
||||||
|
|
||||||
|
override suspend fun getAvailableFilterOptions(): List<ListFilterOption> = buildList {
|
||||||
|
add(ListFilterOption.Downloaded)
|
||||||
|
if (settings.isTrackerEnabled) {
|
||||||
|
add(ListFilterOption.Macro.NEW_CHAPTERS)
|
||||||
|
}
|
||||||
|
add(ListFilterOption.Macro.COMPLETED)
|
||||||
|
add(ListFilterOption.Macro.FAVORITE)
|
||||||
|
repository.getPopularTags(3).mapTo(this) {
|
||||||
|
ListFilterOption.Tag(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,7 +16,6 @@ import org.koitharu.kotatsu.core.ui.util.MenuInvalidator
|
|||||||
import org.koitharu.kotatsu.core.util.ext.addMenuProvider
|
import org.koitharu.kotatsu.core.util.ext.addMenuProvider
|
||||||
import org.koitharu.kotatsu.core.util.ext.observe
|
import org.koitharu.kotatsu.core.util.ext.observe
|
||||||
import org.koitharu.kotatsu.databinding.FragmentListBinding
|
import org.koitharu.kotatsu.databinding.FragmentListBinding
|
||||||
import org.koitharu.kotatsu.list.domain.ListFilterOption
|
|
||||||
import org.koitharu.kotatsu.list.ui.MangaListFragment
|
import org.koitharu.kotatsu.list.ui.MangaListFragment
|
||||||
import org.koitharu.kotatsu.list.ui.size.DynamicItemSizeResolver
|
import org.koitharu.kotatsu.list.ui.size.DynamicItemSizeResolver
|
||||||
|
|
||||||
@@ -35,10 +34,6 @@ class HistoryListFragment : MangaListFragment() {
|
|||||||
|
|
||||||
override fun onScrolledToEnd() = viewModel.requestMoreItems()
|
override fun onScrolledToEnd() = viewModel.requestMoreItems()
|
||||||
|
|
||||||
override fun onFilterOptionClick(option: ListFilterOption) {
|
|
||||||
viewModel.onFilterOptionClick(option)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onEmptyActionClick() {
|
override fun onEmptyActionClick() {
|
||||||
startActivity(NetworkManageIntent())
|
startActivity(NetworkManageIntent())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,9 @@ package org.koitharu.kotatsu.history.ui
|
|||||||
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
|
||||||
@@ -21,31 +24,30 @@ 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.prefs.observeAsStateFlow
|
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.ui.widgets.ChipsView
|
|
||||||
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.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
|
||||||
|
import org.koitharu.kotatsu.history.domain.HistoryListQuickFilter
|
||||||
import org.koitharu.kotatsu.history.domain.MarkAsReadUseCase
|
import org.koitharu.kotatsu.history.domain.MarkAsReadUseCase
|
||||||
import org.koitharu.kotatsu.history.domain.model.MangaWithHistory
|
import org.koitharu.kotatsu.history.domain.model.MangaWithHistory
|
||||||
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.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.EmptyHint
|
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
|
||||||
import org.koitharu.kotatsu.list.ui.model.LoadingState
|
import org.koitharu.kotatsu.list.ui.model.LoadingState
|
||||||
import org.koitharu.kotatsu.list.ui.model.QuickFilter
|
|
||||||
import org.koitharu.kotatsu.list.ui.model.TipModel
|
import org.koitharu.kotatsu.list.ui.model.TipModel
|
||||||
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.local.data.LocalMangaRepository
|
||||||
import org.koitharu.kotatsu.parsers.model.Manga
|
import org.koitharu.kotatsu.parsers.model.Manga
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
import java.util.EnumSet
|
|
||||||
import java.util.concurrent.atomic.AtomicBoolean
|
import java.util.concurrent.atomic.AtomicBoolean
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@@ -58,9 +60,10 @@ class HistoryListViewModel @Inject constructor(
|
|||||||
private val mangaListMapper: MangaListMapper,
|
private val mangaListMapper: MangaListMapper,
|
||||||
private val localMangaRepository: LocalMangaRepository,
|
private val localMangaRepository: LocalMangaRepository,
|
||||||
private val markAsReadUseCase: MarkAsReadUseCase,
|
private val markAsReadUseCase: MarkAsReadUseCase,
|
||||||
|
private val quickFilter: HistoryListQuickFilter,
|
||||||
networkState: NetworkState,
|
networkState: NetworkState,
|
||||||
downloadScheduler: DownloadWorker.Scheduler,
|
downloadScheduler: DownloadWorker.Scheduler,
|
||||||
) : MangaListViewModel(settings, downloadScheduler) {
|
) : MangaListViewModel(settings, downloadScheduler), QuickFilterListener by quickFilter {
|
||||||
|
|
||||||
private val sortOrder: StateFlow<ListSortOrder> = settings.observeAsStateFlow(
|
private val sortOrder: StateFlow<ListSortOrder> = settings.observeAsStateFlow(
|
||||||
scope = viewModelScope + Dispatchers.IO,
|
scope = viewModelScope + Dispatchers.IO,
|
||||||
@@ -68,8 +71,6 @@ class HistoryListViewModel @Inject constructor(
|
|||||||
valueProducer = { historySortOrder },
|
valueProducer = { historySortOrder },
|
||||||
)
|
)
|
||||||
|
|
||||||
private val filterOptions = MutableStateFlow<Set<ListFilterOption>>(EnumSet.noneOf(ListFilterOption::class.java))
|
|
||||||
|
|
||||||
override val listMode = settings.observeAsStateFlow(
|
override val listMode = settings.observeAsStateFlow(
|
||||||
scope = viewModelScope + Dispatchers.Default,
|
scope = viewModelScope + Dispatchers.Default,
|
||||||
key = AppSettings.KEY_LIST_MODE_HISTORY,
|
key = AppSettings.KEY_LIST_MODE_HISTORY,
|
||||||
@@ -93,7 +94,7 @@ class HistoryListViewModel @Inject constructor(
|
|||||||
)
|
)
|
||||||
|
|
||||||
override val content = combine(
|
override val content = combine(
|
||||||
filterOptions,
|
quickFilter.appliedOptions,
|
||||||
observeHistory(),
|
observeHistory(),
|
||||||
isGroupingEnabled,
|
isGroupingEnabled,
|
||||||
observeListModeWithTriggers(),
|
observeListModeWithTriggers(),
|
||||||
@@ -105,7 +106,7 @@ class HistoryListViewModel @Inject constructor(
|
|||||||
if (filters.isEmpty()) {
|
if (filters.isEmpty()) {
|
||||||
listOf(getEmptyState(hasFilters = false))
|
listOf(getEmptyState(hasFilters = false))
|
||||||
} else {
|
} else {
|
||||||
listOf(filterItem(filters), getEmptyState(hasFilters = true))
|
listOf(quickFilter.filterItem(filters), getEmptyState(hasFilters = true))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -118,8 +119,8 @@ class HistoryListViewModel @Inject constructor(
|
|||||||
loadingCounter.increment()
|
loadingCounter.increment()
|
||||||
}.onFirst {
|
}.onFirst {
|
||||||
loadingCounter.decrement()
|
loadingCounter.decrement()
|
||||||
}.catch {
|
}.catch { e ->
|
||||||
emit(listOf(it.toErrorState(canRetry = false)))
|
emit(listOf(e.toErrorState(canRetry = false)))
|
||||||
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, listOf(LoadingState))
|
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, listOf(LoadingState))
|
||||||
|
|
||||||
override fun onRefresh() = Unit
|
override fun onRefresh() = Unit
|
||||||
@@ -161,29 +162,24 @@ class HistoryListViewModel @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onFilterOptionClick(option: ListFilterOption) {
|
private fun observeHistory() = combine(sortOrder, quickFilter.appliedOptions, limit, ::Triple)
|
||||||
filterOptions.value = EnumSet.copyOf(filterOptions.value).also {
|
.flatMapLatest { repository.observeAllWithHistory(it.first, it.second - ListFilterOption.Downloaded, it.third) }
|
||||||
if (option in it) {
|
|
||||||
it.remove(option)
|
|
||||||
} else {
|
|
||||||
it.add(option)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun observeHistory() = combine(sortOrder, filterOptions, limit, ::Triple)
|
|
||||||
.flatMapLatest { repository.observeAllWithHistory(it.first, it.second - ListFilterOption.DOWNLOADED, it.third) }
|
|
||||||
|
|
||||||
private suspend fun mapList(
|
private suspend fun mapList(
|
||||||
filters: Set<ListFilterOption>,
|
filters: Set<ListFilterOption>,
|
||||||
list: List<MangaWithHistory>,
|
historyList: List<MangaWithHistory>,
|
||||||
grouped: Boolean,
|
grouped: Boolean,
|
||||||
mode: ListMode,
|
mode: ListMode,
|
||||||
isOnline: Boolean,
|
isOnline: Boolean,
|
||||||
isIncognito: Boolean,
|
isIncognito: Boolean,
|
||||||
): List<ListModel> {
|
): List<ListModel> {
|
||||||
|
val list = if (!isOnline || ListFilterOption.Downloaded in filters) {
|
||||||
|
historyList.mapToLocal()
|
||||||
|
} else {
|
||||||
|
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) + 3)
|
||||||
result += filterItem(filters)
|
result += quickFilter.filterItem(filters)
|
||||||
if (isIncognito) {
|
if (isIncognito) {
|
||||||
result += TipModel(
|
result += TipModel(
|
||||||
key = AppSettings.KEY_INCOGNITO_MODE,
|
key = AppSettings.KEY_INCOGNITO_MODE,
|
||||||
@@ -205,12 +201,7 @@ class HistoryListViewModel @Inject constructor(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
var isEmpty = true
|
var isEmpty = true
|
||||||
for ((m, history) in list) {
|
for ((manga, history) in list) {
|
||||||
val manga = if ((!isOnline && !m.isLocal) || ListFilterOption.DOWNLOADED in filters) {
|
|
||||||
localMangaRepository.findSavedManga(m)?.manga ?: continue
|
|
||||||
} else {
|
|
||||||
m
|
|
||||||
}
|
|
||||||
isEmpty = false
|
isEmpty = false
|
||||||
if (grouped) {
|
if (grouped) {
|
||||||
val header = history.header(order)
|
val header = history.header(order)
|
||||||
@@ -229,6 +220,20 @@ class HistoryListViewModel @Inject constructor(
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private suspend fun List<MangaWithHistory>.mapToLocal() = coroutineScope {
|
||||||
|
map {
|
||||||
|
async {
|
||||||
|
if (it.manga.isLocal) {
|
||||||
|
it
|
||||||
|
} else {
|
||||||
|
localMangaRepository.findSavedManga(it.manga)?.let { localManga ->
|
||||||
|
MangaWithHistory(localManga.manga, it.history)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.awaitAll().filterNotNull()
|
||||||
|
}
|
||||||
|
|
||||||
private fun MangaHistory.header(order: ListSortOrder): ListHeader? = when (order) {
|
private fun MangaHistory.header(order: ListSortOrder): ListHeader? = when (order) {
|
||||||
ListSortOrder.LAST_READ,
|
ListSortOrder.LAST_READ,
|
||||||
ListSortOrder.LONG_AGO_READ -> ListHeader(calculateTimeAgo(updatedAt))
|
ListSortOrder.LONG_AGO_READ -> ListHeader(calculateTimeAgo(updatedAt))
|
||||||
@@ -254,18 +259,6 @@ class HistoryListViewModel @Inject constructor(
|
|||||||
ListSortOrder.RATING -> null
|
ListSortOrder.RATING -> null
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun filterItem(selected: Set<ListFilterOption>) = QuickFilter(
|
|
||||||
items = ListFilterOption.HISTORY.map { option ->
|
|
||||||
ChipsView.ChipModel(
|
|
||||||
titleResId = option.titleResId,
|
|
||||||
icon = option.iconResId,
|
|
||||||
isCheckable = true,
|
|
||||||
isChecked = option in selected,
|
|
||||||
data = option,
|
|
||||||
)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
private fun getEmptyState(hasFilters: Boolean) = if (hasFilters) {
|
private fun getEmptyState(hasFilters: Boolean) = if (hasFilters) {
|
||||||
EmptyState(
|
EmptyState(
|
||||||
icon = R.drawable.ic_empty_history,
|
icon = R.drawable.ic_empty_history,
|
||||||
|
|||||||
@@ -3,26 +3,84 @@ package org.koitharu.kotatsu.list.domain
|
|||||||
import androidx.annotation.DrawableRes
|
import androidx.annotation.DrawableRes
|
||||||
import androidx.annotation.StringRes
|
import androidx.annotation.StringRes
|
||||||
import org.koitharu.kotatsu.R
|
import org.koitharu.kotatsu.R
|
||||||
import java.util.EnumSet
|
import org.koitharu.kotatsu.core.model.FavouriteCategory
|
||||||
|
import org.koitharu.kotatsu.parsers.model.MangaTag
|
||||||
|
|
||||||
enum class ListFilterOption(
|
sealed interface ListFilterOption {
|
||||||
@StringRes val titleResId: Int,
|
|
||||||
@DrawableRes val iconResId: Int,
|
|
||||||
) {
|
|
||||||
|
|
||||||
DOWNLOADED(R.string.on_device, R.drawable.ic_storage),
|
@get:StringRes
|
||||||
COMPLETED(R.string.status_completed, R.drawable.ic_state_finished),
|
val titleResId: Int
|
||||||
NEW_CHAPTERS(R.string.new_chapters, R.drawable.ic_updated),
|
|
||||||
FAVORITE(R.string.favourites, R.drawable.ic_heart_outline),
|
|
||||||
;
|
|
||||||
|
|
||||||
companion object {
|
@get:DrawableRes
|
||||||
|
val iconResId: Int
|
||||||
|
|
||||||
val HISTORY: Set<ListFilterOption> = EnumSet.of(
|
val titleText: CharSequence?
|
||||||
DOWNLOADED,
|
|
||||||
NEW_CHAPTERS,
|
val groupKey: String
|
||||||
FAVORITE,
|
|
||||||
COMPLETED,
|
data object Downloaded : ListFilterOption {
|
||||||
)
|
|
||||||
|
override val titleResId: Int
|
||||||
|
get() = R.string.on_device
|
||||||
|
|
||||||
|
override val iconResId: Int
|
||||||
|
get() = R.drawable.ic_storage
|
||||||
|
|
||||||
|
override val titleText: CharSequence?
|
||||||
|
get() = null
|
||||||
|
|
||||||
|
override val groupKey: String
|
||||||
|
get() = "_downloaded"
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class Macro(
|
||||||
|
@StringRes override val titleResId: Int,
|
||||||
|
@DrawableRes override val iconResId: Int,
|
||||||
|
) : ListFilterOption {
|
||||||
|
|
||||||
|
COMPLETED(R.string.status_completed, R.drawable.ic_state_finished),
|
||||||
|
NEW_CHAPTERS(R.string.new_chapters, R.drawable.ic_updated),
|
||||||
|
FAVORITE(R.string.favourites, R.drawable.ic_heart_outline),
|
||||||
|
;
|
||||||
|
|
||||||
|
override val titleText: CharSequence?
|
||||||
|
get() = null
|
||||||
|
|
||||||
|
override val groupKey: String
|
||||||
|
get() = name
|
||||||
|
}
|
||||||
|
|
||||||
|
data class Tag(
|
||||||
|
val tag: MangaTag
|
||||||
|
) : ListFilterOption {
|
||||||
|
|
||||||
|
override val titleResId: Int
|
||||||
|
get() = 0
|
||||||
|
|
||||||
|
override val iconResId: Int
|
||||||
|
get() = R.drawable.ic_tag
|
||||||
|
|
||||||
|
override val titleText: String
|
||||||
|
get() = tag.title
|
||||||
|
|
||||||
|
override val groupKey: String
|
||||||
|
get() = "_tag"
|
||||||
|
}
|
||||||
|
|
||||||
|
data class Favorite(
|
||||||
|
val category: FavouriteCategory
|
||||||
|
) : ListFilterOption {
|
||||||
|
|
||||||
|
override val titleResId: Int
|
||||||
|
get() = 0
|
||||||
|
|
||||||
|
override val iconResId: Int
|
||||||
|
get() = R.drawable.ic_heart_outline
|
||||||
|
|
||||||
|
override val titleText: String
|
||||||
|
get() = category.title
|
||||||
|
|
||||||
|
override val groupKey: String
|
||||||
|
get() = "_favcat"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,50 @@
|
|||||||
|
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.ui.widgets.ChipsView
|
||||||
|
import org.koitharu.kotatsu.list.ui.model.QuickFilter
|
||||||
|
import org.koitharu.kotatsu.parsers.util.SuspendLazy
|
||||||
|
|
||||||
|
abstract class MangaListQuickFilter : QuickFilterListener {
|
||||||
|
|
||||||
|
private val appliedFilter = MutableStateFlow<Set<ListFilterOption>>(emptySet())
|
||||||
|
private val availableFilterOptions = SuspendLazy {
|
||||||
|
getAvailableFilterOptions()
|
||||||
|
}
|
||||||
|
|
||||||
|
val appliedOptions
|
||||||
|
get() = appliedFilter.asStateFlow()
|
||||||
|
|
||||||
|
override fun toggleFilterOption(option: ListFilterOption) {
|
||||||
|
appliedFilter.value = ArraySet(appliedFilter.value).also {
|
||||||
|
if (option in it) {
|
||||||
|
it.remove(option)
|
||||||
|
} else {
|
||||||
|
it.add(option)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun clearFilter() {
|
||||||
|
appliedFilter.value = emptySet()
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun filterItem(
|
||||||
|
selectedOptions: Set<ListFilterOption>,
|
||||||
|
) = QuickFilter(
|
||||||
|
items = availableFilterOptions.tryGet().getOrNull()?.map { option ->
|
||||||
|
ChipsView.ChipModel(
|
||||||
|
title = option.titleText,
|
||||||
|
titleResId = option.titleResId,
|
||||||
|
icon = option.iconResId,
|
||||||
|
isCheckable = true,
|
||||||
|
isChecked = option in selectedOptions,
|
||||||
|
data = option,
|
||||||
|
)
|
||||||
|
}.orEmpty(),
|
||||||
|
)
|
||||||
|
|
||||||
|
protected abstract suspend fun getAvailableFilterOptions(): List<ListFilterOption>
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
package org.koitharu.kotatsu.list.domain
|
||||||
|
|
||||||
|
interface QuickFilterListener {
|
||||||
|
|
||||||
|
fun toggleFilterOption(option: ListFilterOption)
|
||||||
|
|
||||||
|
fun clearFilter()
|
||||||
|
}
|
||||||
@@ -45,6 +45,7 @@ import org.koitharu.kotatsu.details.ui.DetailsActivity
|
|||||||
import org.koitharu.kotatsu.download.ui.worker.DownloadStartedObserver
|
import org.koitharu.kotatsu.download.ui.worker.DownloadStartedObserver
|
||||||
import org.koitharu.kotatsu.favourites.ui.categories.select.FavoriteSheet
|
import org.koitharu.kotatsu.favourites.ui.categories.select.FavoriteSheet
|
||||||
import org.koitharu.kotatsu.list.domain.ListFilterOption
|
import org.koitharu.kotatsu.list.domain.ListFilterOption
|
||||||
|
import org.koitharu.kotatsu.list.domain.QuickFilterListener
|
||||||
import org.koitharu.kotatsu.list.ui.adapter.ListItemType
|
import org.koitharu.kotatsu.list.ui.adapter.ListItemType
|
||||||
import org.koitharu.kotatsu.list.ui.adapter.MangaListAdapter
|
import org.koitharu.kotatsu.list.ui.adapter.MangaListAdapter
|
||||||
import org.koitharu.kotatsu.list.ui.adapter.MangaListListener
|
import org.koitharu.kotatsu.list.ui.adapter.MangaListListener
|
||||||
@@ -227,7 +228,9 @@ abstract class MangaListFragment :
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onFilterOptionClick(option: ListFilterOption) = Unit
|
override fun onFilterOptionClick(option: ListFilterOption) {
|
||||||
|
(viewModel as? QuickFilterListener)?.toggleFilterOption(option)
|
||||||
|
}
|
||||||
|
|
||||||
override fun onFilterClick(view: View?) = Unit
|
override fun onFilterClick(view: View?) = Unit
|
||||||
|
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ abstract class MangaListViewModel(
|
|||||||
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
|
||||||
}.onStart { emit("") }
|
}.onStart { emit("") },
|
||||||
) { mode, _ ->
|
) { mode, _ ->
|
||||||
mode
|
mode
|
||||||
}
|
}
|
||||||
|
|||||||
12
app/src/main/res/drawable/ic_tag.xml
Normal file
12
app/src/main/res/drawable/ic_tag.xml
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<vector
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:tint="?attr/colorControlNormal"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path
|
||||||
|
android:fillColor="#000000"
|
||||||
|
android:pathData="M21.41 11.58L12.41 2.58A2 2 0 0 0 11 2H4A2 2 0 0 0 2 4V11A2 2 0 0 0 2.59 12.42L11.59 21.42A2 2 0 0 0 13 22A2 2 0 0 0 14.41 21.41L21.41 14.41A2 2 0 0 0 22 13A2 2 0 0 0 21.41 11.58M13 20L4 11V4H11L20 13M6.5 5A1.5 1.5 0 1 1 5 6.5A1.5 1.5 0 0 1 6.5 5Z" />
|
||||||
|
</vector>
|
||||||
Reference in New Issue
Block a user