From 573768482f9f9f265079736e01f5f467d32110f2 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 (cherry picked from commit 687f3d48ea03e09c2fe33a8d8e36e1adb87bbf4b) # Conflicts: # app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt # app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryPresenter.kt # app/src/main/java/eu/kanade/tachiyomi/ui/library/LibrarySettingsSheet.kt # app/src/main/java/eu/kanade/tachiyomi/widget/ExtendedNavigationView.kt --- .../java/eu/kanade/tachiyomi/Migrations.kt | 20 +++ .../data/preference/PreferenceKeys.kt | 12 +- .../data/preference/PreferencesHelper.kt | 14 +-- .../tachiyomi/ui/library/LibraryPresenter.kt | 115 +++++++++++------- .../ui/library/LibrarySettingsSheet.kt | 53 ++++---- .../widget/ExtendedNavigationView.kt | 47 +++---- 6 files changed, 148 insertions(+), 113 deletions(-) diff --git a/app/src/main/java/eu/kanade/tachiyomi/Migrations.kt b/app/src/main/java/eu/kanade/tachiyomi/Migrations.kt index 11327e106..6c1688f25 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 { @@ -92,6 +95,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 4d3365c68..b97e197e0 100755 --- 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,17 +109,17 @@ 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 filterStarted = "pref_filter_started_key" + const val filterStarted = "pref_filter_library_started" - const val filterTracked = "pref_filter_tracked_key" + const val filterTracked = "pref_filter_library_tracked" - const val filterLewd = "pref_filter_lewd_key" + const val filterLewd = "pref_filter_library_lewd" 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 24d452b7d..43fb694b1 100755 --- 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,18 +211,17 @@ class PreferencesHelper(val context: Context) { fun categoryTabs() = flowPrefs.getBoolean(Keys.categoryTabs, true) - // J2K converted from boolean to integer - fun filterDownloaded() = flowPrefs.getInt(Keys.filterDownloaded, 0) + fun filterDownloaded() = flowPrefs.getInt(Keys.filterDownloaded, ExtendedNavigationView.Item.TriStateGroup.STATE_IGNORE) - fun filterUnread() = flowPrefs.getInt(Keys.filterUnread, 0) + fun filterUnread() = flowPrefs.getInt(Keys.filterUnread, ExtendedNavigationView.Item.TriStateGroup.STATE_IGNORE) - fun filterCompleted() = flowPrefs.getInt(Keys.filterCompleted, 0) + fun filterCompleted() = flowPrefs.getInt(Keys.filterCompleted, ExtendedNavigationView.Item.TriStateGroup.STATE_IGNORE) - fun filterStarted() = flowPrefs.getInt(Keys.filterStarted, 0) + fun filterStarted() = flowPrefs.getInt(Keys.filterStarted, ExtendedNavigationView.Item.TriStateGroup.STATE_IGNORE) - fun filterTracked() = flowPrefs.getInt(Keys.filterTracked, 0) + fun filterTracked() = flowPrefs.getInt(Keys.filterTracked, ExtendedNavigationView.Item.TriStateGroup.STATE_IGNORE) - fun filterLewd() = flowPrefs.getInt(Keys.filterLewd, 0) + fun filterLewd() = flowPrefs.getInt(Keys.filterLewd, 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 e166e643e..da9e15e07 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryPresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryPresenter.kt @@ -13,19 +13,18 @@ import eu.kanade.tachiyomi.data.download.DownloadManager import eu.kanade.tachiyomi.data.library.CustomMangaManager import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.track.TrackManager +import eu.kanade.tachiyomi.source.LocalSource import eu.kanade.tachiyomi.source.SourceManager -import eu.kanade.tachiyomi.source.model.Filter.TriState.Companion.STATE_EXCLUDE -import eu.kanade.tachiyomi.source.model.Filter.TriState.Companion.STATE_IGNORE -import eu.kanade.tachiyomi.source.model.Filter.TriState.Companion.STATE_INCLUDE import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.source.online.all.MergedSource 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 exh.EH_SOURCE_ID import exh.EXH_SOURCE_ID import exh.MERGED_SOURCE_ID @@ -164,8 +163,8 @@ class LibraryPresenter( * @param map the map to filter. */ private fun applyFilters(map: LibraryMap): LibraryMap { + val downloadedOnly = preferences.downloadedOnly().get() val filterDownloaded = preferences.filterDownloaded().get() - val filterDownloadedOnly = preferences.downloadedOnly().get() val filterUnread = preferences.filterUnread().get() val filterCompleted = preferences.filterCompleted().get() // SY --> @@ -174,49 +173,71 @@ class LibraryPresenter( val filterLewd = preferences.filterLewd().get() // SY <-- - val filterFn: (LibraryItem) -> Boolean = f@{ item -> - // Filter when there isn't unread chapters. - if (filterUnread == STATE_INCLUDE && 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 && !downloadedOnly) 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 (filterUnread == STATE_EXCLUDE && item.manga.unread > 0) { - return@f false - } - if (filterCompleted == STATE_INCLUDE && item.manga.status != SManga.COMPLETED) { - return@f false - } - if (filterCompleted == STATE_EXCLUDE && item.manga.status == SManga.COMPLETED) { - return@f false - } - // SY --> - if (filterStarted == STATE_INCLUDE && item.manga.read == 0) { - return@f false - } - if (filterStarted == STATE_EXCLUDE && item.manga.read > 0) { - return@f false - } - if (filterTracked != STATE_IGNORE) { - val tracks = db.getTracks(item.manga).executeAsBlocking() - .filterNot { it.sync_id == TrackManager.MDLIST && it.status == FollowStatus.UNFOLLOWED.int } - if (filterTracked == STATE_INCLUDE && tracks.isEmpty()) return@f false - else if (filterTracked == STATE_EXCLUDE && tracks.isNotEmpty()) return@f false - } - if (filterLewd != STATE_IGNORE) { - val isLewd = item.manga.isLewd() - if (filterLewd == STATE_INCLUDE && !isLewd) return@f false - else if (filterLewd == STATE_EXCLUDE && isLewd) return@f false - } - // Filter when there are no downloads. - if (filterDownloaded != STATE_IGNORE || filterDownloadedOnly) { - val isDownloaded = when { - item.manga.isLocal() -> true - item.downloadCount != -1 -> item.downloadCount > 0 - else -> downloadManager.getDownloadCount(item.manga) > 0 - } - return@f if (filterDownloaded == STATE_INCLUDE || filterDownloadedOnly) isDownloaded else !isDownloaded - } - // SY <-- - true + + return@downloaded if (downloadedOnly || filterDownloaded == STATE_INCLUDE) isDownloaded + else !isDownloaded + } + + // SY --> + val filterFnStarted: (LibraryItem) -> Boolean = started@{ item -> + if (filterStarted == STATE_IGNORE) return@started true + val hasRead = item.manga.read != 0 + + return@started if (filterStarted == STATE_INCLUDE) hasRead + else !hasRead + } + + val filterFnTracked: (LibraryItem) -> Boolean = tracked@{ item -> + if (filterTracked == STATE_IGNORE) return@tracked true + val hasTracks = db.getTracks(item.manga).executeAsBlocking().filterNot { it.sync_id == TrackManager.MDLIST && it.status == FollowStatus.UNFOLLOWED.int }.isNotEmpty() + + return@tracked if (filterTracked == STATE_INCLUDE) hasTracks + else !hasTracks + } + + val filterFnLewd: (LibraryItem) -> Boolean = lewd@{ item -> + if (filterLewd == STATE_IGNORE) return@lewd true + val isLewd = item.manga.isLewd() + + return@lewd if (filterLewd == STATE_INCLUDE) isLewd + else !isLewd + } + // SY <-- + + val filterFn: (LibraryItem) -> Boolean = filter@{ item -> + return@filter !( + !filterFnUnread(item) || + !filterFnCompleted(item) || + !filterFnDownloaded(item) || + // SY --> + !filterFnStarted(item) || + !filterFnTracked(item) || + !filterFnLewd(item) + // SY <-- + ) } 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 a822c8420..f736a4ee9 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 @@ -9,10 +9,10 @@ import eu.kanade.tachiyomi.data.database.DatabaseHelper import eu.kanade.tachiyomi.data.preference.PreferenceValues.DisplayMode import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.track.TrackManager -import eu.kanade.tachiyomi.source.model.Filter.TriState.Companion.STATE_EXCLUDE -import eu.kanade.tachiyomi.source.model.Filter.TriState.Companion.STATE_IGNORE -import eu.kanade.tachiyomi.source.model.Filter.TriState.Companion.STATE_INCLUDE 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.Injekt import uy.kohesive.injekt.api.get @@ -76,21 +76,17 @@ class LibrarySettingsSheet( * Returns true if there's at least one filter from [FilterGroup] active. */ fun hasActiveFilters(): Boolean { - // SY --> - return filterGroup.items.any { it.state != STATE_IGNORE } - // SY <-- + return filterGroup.items.any { it.state != Item.TriStateGroup.STATE_IGNORE } } inner class FilterGroup : Group { - // SY --> 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) private val started = Item.TriStateGroup(R.string.started, this) private val tracked = Item.TriStateGroup(R.string.tracked, this) private val lewd = Item.TriStateGroup(R.string.lewd, this) - // SY <-- override val header = null // SY --> @@ -104,44 +100,37 @@ class LibrarySettingsSheet( // SY <-- override val footer = null - // SY --> - override fun initModels() { // j2k changes - try { + override fun initModels() { + 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() - completed.state = preferences.filterStarted().get() - if (Injekt.get().hasLoggedServices()) { - tracked.state = preferences.filterTracked().get() - } else { - tracked.state = STATE_IGNORE - } - lewd.state = preferences.filterLewd().get() - } catch (e: Exception) { - preferences.upgradeFilters() } + unread.state = preferences.filterUnread().get() + completed.state = preferences.filterCompleted().get() } - override fun onItemClicked(item: Item) { // j2k changes + override fun onItemClicked(item: Item) { item as Item.TriStateGroup val newState = when (item.state) { STATE_IGNORE -> STATE_INCLUDE STATE_INCLUDE -> STATE_EXCLUDE - else -> STATE_IGNORE + STATE_EXCLUDE -> STATE_IGNORE + else -> throw Exception("Unknown State") } item.state = newState when (item) { - downloaded -> preferences.filterDownloaded().set(item.state) - unread -> preferences.filterUnread().set(item.state) - completed -> preferences.filterCompleted().set(item.state) - started -> preferences.filterStarted().set(item.state) - tracked -> preferences.filterTracked().set(item.state) - lewd -> preferences.filterLewd().set(item.state) + downloaded -> preferences.filterDownloaded().set(newState) + unread -> preferences.filterUnread().set(newState) + completed -> preferences.filterCompleted().set(newState) + started -> preferences.filterStarted().set(newState) + tracked -> preferences.filterTracked().set(newState) + lewd -> preferences.filterLewd().set(newState) } adapter.notifyItemChanged(item) } - // SY <-- } } @@ -155,11 +144,13 @@ class LibrarySettingsSheet( setGroups(listOf(SortGroup())) } + // SY --> fun refreshMode() { recycler.adapter = null removeView(recycler) setGroups(listOf(SortGroup())) } + // SY <-- inner class SortGroup : Group { 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 07f094c02..9c45ef1c0 100755 --- 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,21 +72,19 @@ open class ExtendedNavigationView @JvmOverloads constructor( * @param context any context. * @param resId the vector resource to load and tint */ - // SY --> - fun tintVector(context: Context, resId: Int, colorId: Int = R.attr.colorAccent): Drawable { + fun tintVector(context: Context, resId: Int, @AttrRes colorAttrRes: Int = R.attr.colorAccent): Drawable { return AppCompatResources.getDrawable(context, resId)!!.apply { - setTint(context.getResourceColor(colorId)) + setTint(context.getResourceColor(if (enabled) colorAttrRes else R.attr.colorControlNormal)) } } - // SY <-- } /** * 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). @@ -124,7 +123,11 @@ open class ExtendedNavigationView @JvmOverloads constructor( } } } + // SY <-- + /** + * A checkbox with 3 states (unselected, checked, explicitly unchecked). + */ class TriStateGroup(resId: Int, group: Group) : MultiStateGroup(resId, group) { companion object { @@ -135,21 +138,13 @@ open class ExtendedNavigationView @JvmOverloads constructor( 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, - android.R.attr.textColorSecondary - ) - else -> tintVector( - context, - R.drawable.ic_check_box_outline_blank_24dp, - android.R.attr.textColorSecondary - ) + STATE_EXCLUDE -> tintVector(context, R.drawable.ic_check_box_x_24dp) + else -> throw Exception("Unknown state") } } } - // SY <-- } /** @@ -258,13 +253,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 } @@ -273,6 +270,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 } } }