From 4f1275ac019c60f60eb503ba17ec3014d36a974b Mon Sep 17 00:00:00 2001 From: arkon Date: Sun, 4 Apr 2021 16:48:39 -0400 Subject: [PATCH] Allow excluding categories from library update Closes #3467, #4661, #1839 Supersedes #4474 --- .../data/library/LibraryUpdateService.kt | 11 +- .../data/preference/PreferenceKeys.kt | 1 + .../data/preference/PreferencesHelper.kt | 1 + .../ui/setting/SettingsLibraryController.kt | 76 +++++-- .../MaterialDialogMultiChoiceExt.kt | 26 +++ .../QuadStateCheckBox.kt | 13 +- .../QuadStateMultiChoiceDialogAdapter.kt | 187 ++++++++++++++++++ .../QuadStateMultiChoiceViewHolder.kt | 28 +++ .../md_listitem_quadstatemultichoice.xml | 15 ++ app/src/main/res/values/strings.xml | 3 + 10 files changed, 334 insertions(+), 27 deletions(-) create mode 100644 app/src/main/java/eu/kanade/tachiyomi/widget/materialdialogs/MaterialDialogMultiChoiceExt.kt rename app/src/main/java/eu/kanade/tachiyomi/widget/{ => materialdialogs}/QuadStateCheckBox.kt (87%) create mode 100644 app/src/main/java/eu/kanade/tachiyomi/widget/materialdialogs/QuadStateMultiChoiceDialogAdapter.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/widget/materialdialogs/QuadStateMultiChoiceViewHolder.kt create mode 100644 app/src/main/res/layout/md_listitem_quadstatemultichoice.xml diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateService.kt b/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateService.kt index 5410191ca4..73c3fe7631 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateService.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateService.kt @@ -232,11 +232,20 @@ class LibraryUpdateService( libraryManga.filter { it.category == categoryId } } else { val categoriesToUpdate = preferences.libraryUpdateCategories().get().map(String::toInt) - if (categoriesToUpdate.isNotEmpty()) { + val listToInclude = if (categoriesToUpdate.isNotEmpty()) { libraryManga.filter { it.category in categoriesToUpdate } } else { libraryManga } + + val categoriesToExclude = preferences.libraryUpdateCategoriesExclude().get().map(String::toInt) + val listToExclude = if (categoriesToExclude.isNotEmpty()) { + listToInclude.filter { it.category in categoriesToExclude } + } else { + emptyList() + } + + listToInclude.minus(listToExclude) } if (target == Target.CHAPTERS && preferences.updateOnlyNonCompleted()) { listToUpdate = listToUpdate.filterNot { it.status == SManga.COMPLETED } 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 622d5da23a..ecebf6bc91 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 @@ -124,6 +124,7 @@ object PreferenceKeys { const val libraryUpdateRestriction = "library_update_restriction" const val libraryUpdateCategories = "library_update_categories" + const val libraryUpdateCategoriesExclude = "library_update_categories_exclude" const val libraryUpdatePrioritization = "library_update_prioritization" 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 fb7203cc29..9d6c687804 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 @@ -218,6 +218,7 @@ class PreferencesHelper(val context: Context) { fun libraryUpdateRestriction() = prefs.getStringSet(Keys.libraryUpdateRestriction, setOf("wifi")) fun libraryUpdateCategories() = flowPrefs.getStringSet(Keys.libraryUpdateCategories, emptySet()) + fun libraryUpdateCategoriesExclude() = flowPrefs.getStringSet(Keys.libraryUpdateCategoriesExclude, emptySet()) fun libraryUpdatePrioritization() = flowPrefs.getInt(Keys.libraryUpdatePrioritization, 0) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsLibraryController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsLibraryController.kt index 1f2a9ea7ee..bc431e56b0 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsLibraryController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsLibraryController.kt @@ -4,10 +4,10 @@ import android.app.Dialog import android.os.Bundle import android.os.Handler import android.view.View +import androidx.core.text.buildSpannedString import androidx.preference.PreferenceScreen import com.afollestad.materialdialogs.MaterialDialog import com.afollestad.materialdialogs.customview.customView -import com.afollestad.materialdialogs.list.listItemsMultiChoice import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.database.DatabaseHelper import eu.kanade.tachiyomi.data.database.models.Category @@ -29,6 +29,8 @@ import eu.kanade.tachiyomi.util.preference.summaryRes import eu.kanade.tachiyomi.util.preference.switchPreference import eu.kanade.tachiyomi.util.preference.titleRes import eu.kanade.tachiyomi.widget.MinMaxNumberPicker +import eu.kanade.tachiyomi.widget.materialdialogs.QuadStateCheckBox +import eu.kanade.tachiyomi.widget.materialdialogs.listItemsQuadStateMultiChoice import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach @@ -174,18 +176,37 @@ class SettingsLibraryController : SettingsController() { LibraryGlobalUpdateCategoriesDialog().showDialog(router) } - preferences.libraryUpdateCategories().asFlow() - .onEach { mutableSet -> - val selectedCategories = mutableSet - .mapNotNull { id -> categories.find { it.id == id.toInt() } } - .sortedBy { it.order } - - summary = if (selectedCategories.isEmpty()) { - context.getString(R.string.all) - } else { - selectedCategories.joinToString { it.name } - } + fun updateSummary() { + val selectedCategories = preferences.libraryUpdateCategories().get() + .mapNotNull { id -> categories.find { it.id == id.toInt() } } + .sortedBy { it.order } + val includedItemsText = if (selectedCategories.isEmpty()) { + context.getString(R.string.all) + } else { + selectedCategories.joinToString { it.name } } + + val excludedCategories = preferences.libraryUpdateCategoriesExclude().get() + .mapNotNull { id -> categories.find { it.id == id.toInt() } } + .sortedBy { it.order } + val excludedItemsText = if (excludedCategories.isEmpty()) { + context.getString(R.string.none) + } else { + excludedCategories.joinToString { it.name } + } + + summary = buildSpannedString { + append(context.getString(R.string.include, includedItemsText)) + appendLine() + append(context.getString(R.string.exclude, excludedItemsText)) + } + } + + preferences.libraryUpdateCategories().asFlow() + .onEach { updateSummary() } + .launchIn(viewScope) + preferences.libraryUpdateCategoriesExclude().asFlow() + .onEach { updateSummary() } .launchIn(viewScope) } intListPreference { @@ -281,19 +302,34 @@ class SettingsLibraryController : SettingsController() { val items = categories.map { it.name } val preselected = categories - .filter { it.id.toString() in preferences.libraryUpdateCategories().get() } - .map { categories.indexOf(it) } + .map { + when (it.id.toString()) { + in preferences.libraryUpdateCategories().get() -> QuadStateCheckBox.State.CHECKED.ordinal + in preferences.libraryUpdateCategoriesExclude().get() -> QuadStateCheckBox.State.INVERSED.ordinal + else -> QuadStateCheckBox.State.UNCHECKED.ordinal + } + } .toIntArray() return MaterialDialog(activity!!) .title(R.string.pref_library_update_categories) - .listItemsMultiChoice( + .listItemsQuadStateMultiChoice( items = items, - initialSelection = preselected, - allowEmptySelection = true - ) { _, selections, _ -> - val newCategories = selections.map { categories[it] } - preferences.libraryUpdateCategories().set(newCategories.map { it.id.toString() }.toSet()) + initialSelected = preselected + ) { selections -> + val included = selections + .mapIndexed { index, value -> if (value == QuadStateCheckBox.State.CHECKED.ordinal) index else null } + .filterNotNull() + .map { categories[it].id.toString() } + .toSet() + val excluded = selections + .mapIndexed { index, value -> if (value == QuadStateCheckBox.State.INVERSED.ordinal) index else null } + .filterNotNull() + .map { categories[it].id.toString() } + .toSet() + + preferences.libraryUpdateCategories().set(included) + preferences.libraryUpdateCategoriesExclude().set(excluded) } .positiveButton(android.R.string.ok) .negativeButton(android.R.string.cancel) diff --git a/app/src/main/java/eu/kanade/tachiyomi/widget/materialdialogs/MaterialDialogMultiChoiceExt.kt b/app/src/main/java/eu/kanade/tachiyomi/widget/materialdialogs/MaterialDialogMultiChoiceExt.kt new file mode 100644 index 0000000000..b44abf88f3 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/widget/materialdialogs/MaterialDialogMultiChoiceExt.kt @@ -0,0 +1,26 @@ +package eu.kanade.tachiyomi.widget.materialdialogs + +import androidx.annotation.CheckResult +import com.afollestad.materialdialogs.MaterialDialog +import com.afollestad.materialdialogs.list.customListAdapter + +/** + * A variant of listItemsMultiChoice that allows for checkboxes that supports 4 states instead. + */ +@CheckResult +fun MaterialDialog.listItemsQuadStateMultiChoice( + items: List, + disabledIndices: IntArray? = null, + initialSelected: IntArray = IntArray(items.size), + selection: QuadStateMultiChoiceListener +): MaterialDialog { + return customListAdapter( + QuadStateMultiChoiceDialogAdapter( + dialog = this, + items = items, + disabledItems = disabledIndices, + initialSelected = initialSelected, + selection = selection + ) + ) +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/widget/QuadStateCheckBox.kt b/app/src/main/java/eu/kanade/tachiyomi/widget/materialdialogs/QuadStateCheckBox.kt similarity index 87% rename from app/src/main/java/eu/kanade/tachiyomi/widget/QuadStateCheckBox.kt rename to app/src/main/java/eu/kanade/tachiyomi/widget/materialdialogs/QuadStateCheckBox.kt index 78b94aa1bd..5dd19a1419 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/widget/QuadStateCheckBox.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/widget/materialdialogs/QuadStateCheckBox.kt @@ -1,4 +1,4 @@ -package eu.kanade.tachiyomi.widget +package eu.kanade.tachiyomi.widget.materialdialogs import android.content.Context import android.graphics.drawable.Drawable @@ -35,10 +35,11 @@ class QuadStateCheckBox @JvmOverloads constructor(context: Context, attrs: Attri } } - sealed class State { - object UNCHECKED : State() - object INDETERMINATE : State() - object CHECKED : State() - object INVERSED : State() + enum class State { + UNCHECKED, + INDETERMINATE, + CHECKED, + INVERSED, + ; } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/widget/materialdialogs/QuadStateMultiChoiceDialogAdapter.kt b/app/src/main/java/eu/kanade/tachiyomi/widget/materialdialogs/QuadStateMultiChoiceDialogAdapter.kt new file mode 100644 index 0000000000..5d40486672 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/widget/materialdialogs/QuadStateMultiChoiceDialogAdapter.kt @@ -0,0 +1,187 @@ +package eu.kanade.tachiyomi.widget.materialdialogs + +import android.view.View +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import com.afollestad.materialdialogs.MaterialDialog +import com.afollestad.materialdialogs.internal.list.DialogAdapter +import com.afollestad.materialdialogs.list.getItemSelector +import com.afollestad.materialdialogs.utils.MDUtil.inflate +import com.afollestad.materialdialogs.utils.MDUtil.maybeSetTextColor +import eu.kanade.tachiyomi.R + +private object CheckPayload +private object InverseCheckPayload +private object UncheckPayload + +typealias QuadStateMultiChoiceListener = (indices: IntArray) -> Unit + +internal class QuadStateMultiChoiceDialogAdapter( + private var dialog: MaterialDialog, + internal var items: List, + disabledItems: IntArray?, + initialSelected: IntArray, + internal var selection: QuadStateMultiChoiceListener +) : RecyclerView.Adapter(), + DialogAdapter { + + private val states = QuadStateCheckBox.State.values() + + private var currentSelection: IntArray = initialSelected + set(value) { + val previousSelection = field + field = value + previousSelection.forEachIndexed { index, previous -> + val current = value[index] + when { + current == QuadStateCheckBox.State.CHECKED.ordinal && previous != QuadStateCheckBox.State.CHECKED.ordinal -> { + // This value was selected + notifyItemChanged(index, CheckPayload) + } + current == QuadStateCheckBox.State.INVERSED.ordinal && previous != QuadStateCheckBox.State.INVERSED.ordinal -> { + // This value was inverse selected + notifyItemChanged(index, InverseCheckPayload) + } + current == QuadStateCheckBox.State.UNCHECKED.ordinal && previous != QuadStateCheckBox.State.UNCHECKED.ordinal -> { + // This value was unselected + notifyItemChanged(index, UncheckPayload) + } + } + } + } + private var disabledIndices: IntArray = disabledItems ?: IntArray(0) + + internal fun itemClicked(index: Int) { + val newSelection = this.currentSelection.toMutableList() + newSelection[index] = when (currentSelection[index]) { + QuadStateCheckBox.State.CHECKED.ordinal -> QuadStateCheckBox.State.INVERSED.ordinal + QuadStateCheckBox.State.INVERSED.ordinal -> QuadStateCheckBox.State.UNCHECKED.ordinal + // INDETERMINATE or UNCHECKED + else -> QuadStateCheckBox.State.CHECKED.ordinal + } + this.currentSelection = newSelection.toIntArray() + } + + override fun onCreateViewHolder( + parent: ViewGroup, + viewType: Int + ): QuadStateMultiChoiceViewHolder { + val listItemView: View = parent.inflate(dialog.windowContext, R.layout.md_listitem_quadstatemultichoice) + val viewHolder = QuadStateMultiChoiceViewHolder( + itemView = listItemView, + adapter = this + ) + viewHolder.titleView.maybeSetTextColor(dialog.windowContext, R.attr.md_color_content) + + return viewHolder + } + + override fun getItemCount() = items.size + + override fun onBindViewHolder( + holder: QuadStateMultiChoiceViewHolder, + position: Int + ) { + holder.isEnabled = !disabledIndices.contains(position) + + holder.controlView.state = states[currentSelection[position]] + holder.titleView.text = items[position] + holder.itemView.background = dialog.getItemSelector() + + if (dialog.bodyFont != null) { + holder.titleView.typeface = dialog.bodyFont + } + } + + override fun onBindViewHolder( + holder: QuadStateMultiChoiceViewHolder, + position: Int, + payloads: MutableList + ) { + when (payloads.firstOrNull()) { + CheckPayload -> { + holder.controlView.state = QuadStateCheckBox.State.CHECKED + return + } + InverseCheckPayload -> { + holder.controlView.state = QuadStateCheckBox.State.INVERSED + return + } + UncheckPayload -> { + holder.controlView.state = QuadStateCheckBox.State.UNCHECKED + return + } + } + super.onBindViewHolder(holder, position, payloads) + } + + override fun positiveButtonClicked() { + selection.invoke(currentSelection) + } + + override fun replaceItems( + items: List, + listener: QuadStateMultiChoiceListener? + ) { + this.items = items + if (listener != null) { + this.selection = listener + } + this.notifyDataSetChanged() + } + + override fun disableItems(indices: IntArray) { + this.disabledIndices = indices + notifyDataSetChanged() + } + + override fun checkItems(indices: IntArray) { + val newSelection = this.currentSelection.toMutableList() + for (index in indices) { + newSelection[index] = QuadStateCheckBox.State.CHECKED.ordinal + } + this.currentSelection = newSelection.toIntArray() + } + + override fun uncheckItems(indices: IntArray) { + val newSelection = this.currentSelection.toMutableList() + for (index in indices) { + newSelection[index] = QuadStateCheckBox.State.UNCHECKED.ordinal + } + this.currentSelection = newSelection.toIntArray() + } + + override fun toggleItems(indices: IntArray) { + val newSelection = this.currentSelection.toMutableList() + for (index in indices) { + if (this.disabledIndices.contains(index)) { + continue + } + + if (this.currentSelection[index] != QuadStateCheckBox.State.CHECKED.ordinal) { + newSelection[index] = QuadStateCheckBox.State.CHECKED.ordinal + } else { + newSelection[index] = QuadStateCheckBox.State.UNCHECKED.ordinal + } + } + this.currentSelection = newSelection.toIntArray() + } + + override fun checkAllItems() { + this.currentSelection = IntArray(itemCount) { QuadStateCheckBox.State.CHECKED.ordinal } + } + + override fun uncheckAllItems() { + this.currentSelection = IntArray(itemCount) { QuadStateCheckBox.State.UNCHECKED.ordinal } + } + + override fun toggleAllChecked() { + if (this.currentSelection.any { it != QuadStateCheckBox.State.CHECKED.ordinal }) { + checkAllItems() + } else { + uncheckAllItems() + } + } + + override fun isItemChecked(index: Int) = this.currentSelection[index] == QuadStateCheckBox.State.CHECKED.ordinal +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/widget/materialdialogs/QuadStateMultiChoiceViewHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/widget/materialdialogs/QuadStateMultiChoiceViewHolder.kt new file mode 100644 index 0000000000..d0cb30e7bd --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/widget/materialdialogs/QuadStateMultiChoiceViewHolder.kt @@ -0,0 +1,28 @@ +package eu.kanade.tachiyomi.widget.materialdialogs + +import android.view.View +import android.widget.TextView +import androidx.recyclerview.widget.RecyclerView +import eu.kanade.tachiyomi.R + +internal class QuadStateMultiChoiceViewHolder( + itemView: View, + private val adapter: QuadStateMultiChoiceDialogAdapter +) : RecyclerView.ViewHolder(itemView), View.OnClickListener { + init { + itemView.setOnClickListener(this) + } + + val controlView: QuadStateCheckBox = itemView.findViewById(R.id.md_quad_state_control) + val titleView: TextView = itemView.findViewById(R.id.md_quad_state_title) + + var isEnabled: Boolean + get() = itemView.isEnabled + set(value) { + itemView.isEnabled = value + controlView.isEnabled = value + titleView.isEnabled = value + } + + override fun onClick(view: View) = adapter.itemClicked(bindingAdapterPosition) +} diff --git a/app/src/main/res/layout/md_listitem_quadstatemultichoice.xml b/app/src/main/res/layout/md_listitem_quadstatemultichoice.xml new file mode 100644 index 0000000000..3967789b75 --- /dev/null +++ b/app/src/main/res/layout/md_listitem_quadstatemultichoice.xml @@ -0,0 +1,15 @@ + + + + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 2cb9e5d2a7..c1bfc7fcbc 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -225,6 +225,9 @@ Categories to include in global update All + None + Include: %s + Exclude: %s All