mirror of
				https://github.com/mihonapp/mihon.git
				synced 2025-10-31 06:17:57 +01:00 
			
		
		
		
	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:
		| @@ -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 | ||||
|         } | ||||
|  | ||||
|         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) } | ||||
|   | ||||
| @@ -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 | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|   | ||||
		Reference in New Issue
	
	Block a user