From 687f3d48ea03e09c2fe33a8d8e36e1adb87bbf4b Mon Sep 17 00:00:00 2001 From: arkon Date: Mon, 14 Sep 2020 17:52:00 -0400 Subject: [PATCH] Tri-state library filters (closes #1814) Based on https://github.com/inorichi/tachiyomi/pull/2127. Co-authored-by: hXtreme --- .../java/eu/kanade/tachiyomi/Migrations.kt | 20 +++++++ .../data/preference/PreferenceKeys.kt | 6 +- .../data/preference/PreferencesHelper.kt | 7 ++- .../tachiyomi/ui/library/LibraryPresenter.kt | 59 +++++++++++-------- .../ui/library/LibrarySettingsSheet.kt | 39 ++++++++---- .../widget/ExtendedNavigationView.kt | 48 ++++++++++++--- 6 files changed, 128 insertions(+), 51 deletions(-) diff --git a/app/src/main/java/eu/kanade/tachiyomi/Migrations.kt b/app/src/main/java/eu/kanade/tachiyomi/Migrations.kt index 3b748d77a2..0d6af6216e 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/Migrations.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/Migrations.kt @@ -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 diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferenceKeys.kt b/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferenceKeys.kt index d9a7781ceb..0d3d974ec8 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferenceKeys.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferenceKeys.kt @@ -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" diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt b/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt index c8961a7873..ce43275517 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt @@ -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) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryPresenter.kt index 75200a0a63..6e689f2185 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryPresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryPresenter.kt @@ -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 + } + + 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 + } + + 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 } - if (filterCompleted && item.manga.status != SManga.COMPLETED) { - return@f false - } + return@downloaded if (downloadedOnly || filterDownloaded == STATE_INCLUDE) isDownloaded + else !isDownloaded + } - // 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 - } - - return@f downloadManager.getDownloadCount(item.manga) > 0 - } - true + val filterFn: (LibraryItem) -> Boolean = filter@{ item -> + return@filter !( + !filterFnUnread(item) || + !filterFnCompleted(item) || + !filterFnDownloaded(item) + ) } return map.mapValues { entry -> entry.value.filter(filterFn) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibrarySettingsSheet.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibrarySettingsSheet.kt index 2491522324..87587b9373 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibrarySettingsSheet.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibrarySettingsSheet.kt @@ -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) diff --git a/app/src/main/java/eu/kanade/tachiyomi/widget/ExtendedNavigationView.kt b/app/src/main/java/eu/kanade/tachiyomi/widget/ExtendedNavigationView.kt index 44b7bd791f..be59f618d5 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/widget/ExtendedNavigationView.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/widget/ExtendedNavigationView.kt @@ -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 } } }