Browse Source

Tri-state library filters (closes #1814)

Based on https://github.com/inorichi/tachiyomi/pull/2127.

Co-authored-by: hXtreme <[email protected]>
arkon 4 years ago
parent
commit
687f3d48ea

+ 20 - 0
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

+ 3 - 3
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"
 

+ 4 - 3
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)
 

+ 35 - 22
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
 
-            if (filterCompleted && item.manga.status != SManga.COMPLETED) {
-                return@f false
-            }
+            return@unread if (filterUnread == STATE_INCLUDE) isUnread
+            else !isUnread
+        }
 
-            // 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 filterFnCompleted: (LibraryItem) -> Boolean = completed@{ item ->
+            if (filterCompleted == STATE_IGNORE) return@completed true
+            val isCompleted = item.manga.status == SManga.COMPLETED
 
-                return@f downloadManager.getDownloadCount(item.manga) > 0
+            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
             }
-            true
+
+            return@downloaded if (downloadedOnly || filterDownloaded == STATE_INCLUDE) isDownloaded
+            else !isDownloaded
+        }
+
+        val filterFn: (LibraryItem) -> Boolean = filter@{ item ->
+            return@filter !(
+                !filterFnUnread(item) ||
+                    !filterFnCompleted(item) ||
+                    !filterFnDownloaded(item)
+                )
         }
 
         return map.mapValues { entry -> entry.value.filter(filterFn) }

+ 26 - 13
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)

+ 39 - 9
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
                 }
             }
         }