Tri-state library filters (closes #1814)
Based on https://github.com/inorichi/tachiyomi/pull/2127. Co-authored-by: hXtreme <hXtreme@users.noreply.github.com>
This commit is contained in:
parent
da5f10a2f1
commit
687f3d48ea
@ -1,11 +1,14 @@
|
||||
package eu.kanade.tachiyomi
|
||||
|
||||
import androidx.core.content.edit
|
||||
import androidx.preference.PreferenceManager
|
||||
import eu.kanade.tachiyomi.data.backup.BackupCreatorJob
|
||||
import eu.kanade.tachiyomi.data.library.LibraryUpdateJob
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.data.updater.UpdaterJob
|
||||
import eu.kanade.tachiyomi.extension.ExtensionUpdateJob
|
||||
import eu.kanade.tachiyomi.ui.library.LibrarySort
|
||||
import eu.kanade.tachiyomi.widget.ExtendedNavigationView
|
||||
import java.io.File
|
||||
|
||||
object Migrations {
|
||||
@ -89,6 +92,23 @@ object Migrations {
|
||||
preferences.librarySortingMode().set(LibrarySort.ALPHA)
|
||||
}
|
||||
}
|
||||
if (oldVersion < 52) {
|
||||
// Migrate library filters to tri-state versions
|
||||
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
fun convertBooleanPrefToTriState(key: String): Int {
|
||||
val oldPrefValue = prefs.getBoolean(key, false)
|
||||
return if (oldPrefValue) ExtendedNavigationView.Item.TriStateGroup.STATE_INCLUDE
|
||||
else ExtendedNavigationView.Item.TriStateGroup.STATE_IGNORE
|
||||
}
|
||||
preferences.filterDownloaded().set(convertBooleanPrefToTriState("pref_filter_downloaded_key"))
|
||||
preferences.filterUnread().set(convertBooleanPrefToTriState("pref_filter_unread_key"))
|
||||
preferences.filterCompleted().set(convertBooleanPrefToTriState("pref_filter_completed_key"))
|
||||
prefs.edit {
|
||||
remove("pref_filter_downloaded_key")
|
||||
remove("pref_filter_unread_key")
|
||||
remove("pref_filter_completed_key")
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
return false
|
||||
|
@ -109,11 +109,11 @@ object PreferenceKeys {
|
||||
|
||||
const val downloadedOnly = "pref_downloaded_only"
|
||||
|
||||
const val filterDownloaded = "pref_filter_downloaded_key"
|
||||
const val filterDownloaded = "pref_filter_library_downloaded"
|
||||
|
||||
const val filterUnread = "pref_filter_unread_key"
|
||||
const val filterUnread = "pref_filter_library_unread"
|
||||
|
||||
const val filterCompleted = "pref_filter_completed_key"
|
||||
const val filterCompleted = "pref_filter_library_completed"
|
||||
|
||||
const val librarySortingMode = "library_sorting_mode"
|
||||
|
||||
|
@ -13,6 +13,7 @@ import eu.kanade.tachiyomi.data.preference.PreferenceValues.DisplayMode
|
||||
import eu.kanade.tachiyomi.data.preference.PreferenceValues.NsfwAllowance
|
||||
import eu.kanade.tachiyomi.data.track.TrackService
|
||||
import eu.kanade.tachiyomi.data.track.anilist.Anilist
|
||||
import eu.kanade.tachiyomi.widget.ExtendedNavigationView
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
@ -210,11 +211,11 @@ class PreferencesHelper(val context: Context) {
|
||||
|
||||
fun categoryTabs() = flowPrefs.getBoolean(Keys.categoryTabs, true)
|
||||
|
||||
fun filterDownloaded() = flowPrefs.getBoolean(Keys.filterDownloaded, false)
|
||||
fun filterDownloaded() = flowPrefs.getInt(Keys.filterDownloaded, ExtendedNavigationView.Item.TriStateGroup.STATE_IGNORE)
|
||||
|
||||
fun filterUnread() = flowPrefs.getBoolean(Keys.filterUnread, false)
|
||||
fun filterUnread() = flowPrefs.getInt(Keys.filterUnread, ExtendedNavigationView.Item.TriStateGroup.STATE_IGNORE)
|
||||
|
||||
fun filterCompleted() = flowPrefs.getBoolean(Keys.filterCompleted, false)
|
||||
fun filterCompleted() = flowPrefs.getInt(Keys.filterCompleted, ExtendedNavigationView.Item.TriStateGroup.STATE_IGNORE)
|
||||
|
||||
fun librarySortingMode() = flowPrefs.getInt(Keys.librarySortingMode, 0)
|
||||
|
||||
|
@ -10,15 +10,17 @@ import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.data.database.models.MangaCategory
|
||||
import eu.kanade.tachiyomi.data.download.DownloadManager
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.source.LocalSource
|
||||
import eu.kanade.tachiyomi.source.SourceManager
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
|
||||
import eu.kanade.tachiyomi.util.isLocal
|
||||
import eu.kanade.tachiyomi.util.lang.combineLatest
|
||||
import eu.kanade.tachiyomi.util.lang.isNullOrUnsubscribed
|
||||
import eu.kanade.tachiyomi.util.lang.launchIO
|
||||
import eu.kanade.tachiyomi.util.removeCovers
|
||||
import eu.kanade.tachiyomi.widget.ExtendedNavigationView.Item.TriStateGroup.Companion.STATE_IGNORE
|
||||
import eu.kanade.tachiyomi.widget.ExtendedNavigationView.Item.TriStateGroup.Companion.STATE_INCLUDE
|
||||
import rx.Observable
|
||||
import rx.Subscription
|
||||
import rx.android.schedulers.AndroidSchedulers
|
||||
@ -110,34 +112,45 @@ class LibraryPresenter(
|
||||
* @param map the map to filter.
|
||||
*/
|
||||
private fun applyFilters(map: LibraryMap): LibraryMap {
|
||||
val filterDownloaded = preferences.downloadedOnly().get() || preferences.filterDownloaded().get()
|
||||
val downloadedOnly = preferences.downloadedOnly().get()
|
||||
val filterDownloaded = preferences.filterDownloaded().get()
|
||||
val filterUnread = preferences.filterUnread().get()
|
||||
val filterCompleted = preferences.filterCompleted().get()
|
||||
|
||||
val filterFn: (LibraryItem) -> Boolean = f@{ item ->
|
||||
// Filter when there isn't unread chapters.
|
||||
if (filterUnread && item.manga.unread == 0) {
|
||||
return@f false
|
||||
val filterFnUnread: (LibraryItem) -> Boolean = unread@{ item ->
|
||||
if (filterUnread == STATE_IGNORE) return@unread true
|
||||
val isUnread = item.manga.unread != 0
|
||||
|
||||
return@unread if (filterUnread == STATE_INCLUDE) isUnread
|
||||
else !isUnread
|
||||
}
|
||||
|
||||
if (filterCompleted && item.manga.status != SManga.COMPLETED) {
|
||||
return@f false
|
||||
val filterFnCompleted: (LibraryItem) -> Boolean = completed@{ item ->
|
||||
if (filterCompleted == STATE_IGNORE) return@completed true
|
||||
val isCompleted = item.manga.status == SManga.COMPLETED
|
||||
|
||||
return@completed if (filterCompleted == STATE_INCLUDE) isCompleted
|
||||
else !isCompleted
|
||||
}
|
||||
|
||||
// Filter when there are no downloads.
|
||||
if (filterDownloaded) {
|
||||
// Local manga are always downloaded
|
||||
if (item.manga.isLocal()) {
|
||||
return@f true
|
||||
}
|
||||
// Don't bother with directory checking if download count has been set.
|
||||
if (item.downloadCount != -1) {
|
||||
return@f item.downloadCount > 0
|
||||
val filterFnDownloaded: (LibraryItem) -> Boolean = downloaded@{ item ->
|
||||
if (filterDownloaded == STATE_IGNORE) return@downloaded true
|
||||
val isDownloaded = when {
|
||||
item.manga.source == LocalSource.ID -> true
|
||||
item.downloadCount != -1 -> item.downloadCount > 0
|
||||
else -> downloadManager.getDownloadCount(item.manga) > 0
|
||||
}
|
||||
|
||||
return@f downloadManager.getDownloadCount(item.manga) > 0
|
||||
return@downloaded if (downloadedOnly || filterDownloaded == STATE_INCLUDE) isDownloaded
|
||||
else !isDownloaded
|
||||
}
|
||||
true
|
||||
|
||||
val filterFn: (LibraryItem) -> Boolean = filter@{ item ->
|
||||
return@filter !(
|
||||
!filterFnUnread(item) ||
|
||||
!filterFnCompleted(item) ||
|
||||
!filterFnDownloaded(item)
|
||||
)
|
||||
}
|
||||
|
||||
return map.mapValues { entry -> entry.value.filter(filterFn) }
|
||||
|
@ -8,6 +8,9 @@ import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.preference.PreferenceValues.DisplayMode
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.widget.ExtendedNavigationView
|
||||
import eu.kanade.tachiyomi.widget.ExtendedNavigationView.Item.TriStateGroup.Companion.STATE_EXCLUDE
|
||||
import eu.kanade.tachiyomi.widget.ExtendedNavigationView.Item.TriStateGroup.Companion.STATE_IGNORE
|
||||
import eu.kanade.tachiyomi.widget.ExtendedNavigationView.Item.TriStateGroup.Companion.STATE_INCLUDE
|
||||
import eu.kanade.tachiyomi.widget.TabbedBottomSheetDialog
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
@ -59,33 +62,43 @@ class LibrarySettingsSheet(
|
||||
* Returns true if there's at least one filter from [FilterGroup] active.
|
||||
*/
|
||||
fun hasActiveFilters(): Boolean {
|
||||
return filterGroup.items.any { it.checked }
|
||||
return filterGroup.items.any { it.state != Item.TriStateGroup.STATE_IGNORE }
|
||||
}
|
||||
|
||||
inner class FilterGroup : Group {
|
||||
|
||||
private val downloaded = Item.CheckboxGroup(R.string.action_filter_downloaded, this)
|
||||
private val unread = Item.CheckboxGroup(R.string.action_filter_unread, this)
|
||||
private val completed = Item.CheckboxGroup(R.string.completed, this)
|
||||
private val downloaded = Item.TriStateGroup(R.string.action_filter_downloaded, this)
|
||||
private val unread = Item.TriStateGroup(R.string.action_filter_unread, this)
|
||||
private val completed = Item.TriStateGroup(R.string.completed, this)
|
||||
|
||||
override val header = null
|
||||
override val items = listOf(downloaded, unread, completed)
|
||||
override val footer = null
|
||||
|
||||
override fun initModels() {
|
||||
downloaded.checked = preferences.downloadedOnly().get() || preferences.filterDownloaded().get()
|
||||
downloaded.enabled = !preferences.downloadedOnly().get()
|
||||
unread.checked = preferences.filterUnread().get()
|
||||
completed.checked = preferences.filterCompleted().get()
|
||||
if (preferences.downloadedOnly().get()) {
|
||||
downloaded.state = STATE_INCLUDE
|
||||
downloaded.enabled = false
|
||||
} else {
|
||||
downloaded.state = preferences.filterDownloaded().get()
|
||||
}
|
||||
unread.state = preferences.filterUnread().get()
|
||||
completed.state = preferences.filterCompleted().get()
|
||||
}
|
||||
|
||||
override fun onItemClicked(item: Item) {
|
||||
item as Item.CheckboxGroup
|
||||
item.checked = !item.checked
|
||||
item as Item.TriStateGroup
|
||||
val newState = when (item.state) {
|
||||
STATE_IGNORE -> STATE_INCLUDE
|
||||
STATE_INCLUDE -> STATE_EXCLUDE
|
||||
STATE_EXCLUDE -> STATE_IGNORE
|
||||
else -> throw Exception("Unknown State")
|
||||
}
|
||||
item.state = newState
|
||||
when (item) {
|
||||
downloaded -> preferences.filterDownloaded().set(item.checked)
|
||||
unread -> preferences.filterUnread().set(item.checked)
|
||||
completed -> preferences.filterCompleted().set(item.checked)
|
||||
downloaded -> preferences.filterDownloaded().set(newState)
|
||||
unread -> preferences.filterUnread().set(newState)
|
||||
completed -> preferences.filterCompleted().set(newState)
|
||||
}
|
||||
|
||||
adapter.notifyItemChanged(item)
|
||||
|
@ -4,6 +4,7 @@ import android.content.Context
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.util.AttributeSet
|
||||
import android.view.ViewGroup
|
||||
import androidx.annotation.AttrRes
|
||||
import androidx.annotation.CallSuper
|
||||
import androidx.appcompat.content.res.AppCompatResources
|
||||
import androidx.core.content.ContextCompat
|
||||
@ -45,20 +46,20 @@ open class ExtendedNavigationView @JvmOverloads constructor(
|
||||
/**
|
||||
* A checkbox belonging to a group. The group must handle selections and restrictions.
|
||||
*/
|
||||
class CheckboxGroup(resTitle: Int, override val group: Group, checked: Boolean = false) :
|
||||
Checkbox(resTitle, checked), GroupedItem
|
||||
class CheckboxGroup(resTitle: Int, override val group: Group, checked: Boolean = false, enabled: Boolean = true) :
|
||||
Checkbox(resTitle, checked, enabled), GroupedItem
|
||||
|
||||
/**
|
||||
* A radio belonging to a group (a sole radio makes no sense). The group must handle
|
||||
* selections and restrictions.
|
||||
*/
|
||||
class Radio(val resTitle: Int, override val group: Group, var checked: Boolean = false) :
|
||||
class Radio(val resTitle: Int, override val group: Group, var checked: Boolean = false, var enabled: Boolean = true) :
|
||||
Item(), GroupedItem
|
||||
|
||||
/**
|
||||
* An item with which needs more than two states (selected/deselected).
|
||||
*/
|
||||
abstract class MultiState(val resTitle: Int, var state: Int = 0) : Item() {
|
||||
abstract class MultiState(val resTitle: Int, var state: Int = 0, var enabled: Boolean = true) : Item() {
|
||||
|
||||
/**
|
||||
* Returns the drawable associated to every possible each state.
|
||||
@ -71,9 +72,9 @@ open class ExtendedNavigationView @JvmOverloads constructor(
|
||||
* @param context any context.
|
||||
* @param resId the vector resource to load and tint
|
||||
*/
|
||||
fun tintVector(context: Context, resId: Int): Drawable {
|
||||
fun tintVector(context: Context, resId: Int, @AttrRes colorAttrRes: Int = R.attr.colorAccent): Drawable {
|
||||
return AppCompatResources.getDrawable(context, resId)!!.apply {
|
||||
setTint(context.getResourceColor(R.attr.colorAccent))
|
||||
setTint(context.getResourceColor(if (enabled) colorAttrRes else R.attr.colorControlNormal))
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -82,8 +83,8 @@ open class ExtendedNavigationView @JvmOverloads constructor(
|
||||
* An item with which needs more than two states (selected/deselected) belonging to a group.
|
||||
* The group must handle selections and restrictions.
|
||||
*/
|
||||
abstract class MultiStateGroup(resTitle: Int, override val group: Group, state: Int = 0) :
|
||||
MultiState(resTitle, state), GroupedItem
|
||||
abstract class MultiStateGroup(resTitle: Int, override val group: Group, state: Int = 0, enabled: Boolean = true) :
|
||||
MultiState(resTitle, state, enabled), GroupedItem
|
||||
|
||||
/**
|
||||
* A multistate item for sorting lists (unselected, ascending, descending).
|
||||
@ -105,6 +106,27 @@ open class ExtendedNavigationView @JvmOverloads constructor(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A checkbox with 3 states (unselected, checked, explicitly unchecked).
|
||||
*/
|
||||
class TriStateGroup(resId: Int, group: Group) : MultiStateGroup(resId, group) {
|
||||
|
||||
companion object {
|
||||
const val STATE_IGNORE = 0
|
||||
const val STATE_INCLUDE = 1
|
||||
const val STATE_EXCLUDE = 2
|
||||
}
|
||||
|
||||
override fun getStateDrawable(context: Context): Drawable? {
|
||||
return when (state) {
|
||||
STATE_IGNORE -> tintVector(context, R.drawable.ic_check_box_outline_blank_24dp, R.attr.colorControlNormal)
|
||||
STATE_INCLUDE -> tintVector(context, R.drawable.ic_check_box_24dp)
|
||||
STATE_EXCLUDE -> tintVector(context, R.drawable.ic_check_box_x_24dp)
|
||||
else -> throw Exception("Unknown state")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -213,13 +235,15 @@ open class ExtendedNavigationView @JvmOverloads constructor(
|
||||
val item = items[position] as Item.Radio
|
||||
holder.radio.setText(item.resTitle)
|
||||
holder.radio.isChecked = item.checked
|
||||
|
||||
holder.itemView.isClickable = item.enabled
|
||||
holder.radio.isEnabled = item.enabled
|
||||
}
|
||||
is CheckboxHolder -> {
|
||||
val item = items[position] as Item.CheckboxGroup
|
||||
holder.check.setText(item.resTitle)
|
||||
holder.check.isChecked = item.checked
|
||||
|
||||
// Allow disabling the holder
|
||||
holder.itemView.isClickable = item.enabled
|
||||
holder.check.isEnabled = item.enabled
|
||||
}
|
||||
@ -228,6 +252,12 @@ open class ExtendedNavigationView @JvmOverloads constructor(
|
||||
val drawable = item.getStateDrawable(context)
|
||||
holder.text.setText(item.resTitle)
|
||||
holder.text.setCompoundDrawablesWithIntrinsicBounds(drawable, null, null, null)
|
||||
|
||||
holder.itemView.isClickable = item.enabled
|
||||
holder.text.isEnabled = item.enabled
|
||||
|
||||
// Mimics checkbox/radio button
|
||||
holder.text.alpha = if (item.enabled) 1f else 0.4f
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user