Allow excluding categories from library update

Closes #3467, #4661, #1839

Supersedes #4474
This commit is contained in:
arkon 2021-04-04 16:48:39 -04:00
parent b2fee7035f
commit 4f1275ac01
10 changed files with 334 additions and 27 deletions

View File

@ -232,11 +232,20 @@ class LibraryUpdateService(
libraryManga.filter { it.category == categoryId } libraryManga.filter { it.category == categoryId }
} else { } else {
val categoriesToUpdate = preferences.libraryUpdateCategories().get().map(String::toInt) val categoriesToUpdate = preferences.libraryUpdateCategories().get().map(String::toInt)
if (categoriesToUpdate.isNotEmpty()) { val listToInclude = if (categoriesToUpdate.isNotEmpty()) {
libraryManga.filter { it.category in categoriesToUpdate } libraryManga.filter { it.category in categoriesToUpdate }
} else { } else {
libraryManga 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()) { if (target == Target.CHAPTERS && preferences.updateOnlyNonCompleted()) {
listToUpdate = listToUpdate.filterNot { it.status == SManga.COMPLETED } listToUpdate = listToUpdate.filterNot { it.status == SManga.COMPLETED }

View File

@ -124,6 +124,7 @@ object PreferenceKeys {
const val libraryUpdateRestriction = "library_update_restriction" const val libraryUpdateRestriction = "library_update_restriction"
const val libraryUpdateCategories = "library_update_categories" const val libraryUpdateCategories = "library_update_categories"
const val libraryUpdateCategoriesExclude = "library_update_categories_exclude"
const val libraryUpdatePrioritization = "library_update_prioritization" const val libraryUpdatePrioritization = "library_update_prioritization"

View File

@ -218,6 +218,7 @@ class PreferencesHelper(val context: Context) {
fun libraryUpdateRestriction() = prefs.getStringSet(Keys.libraryUpdateRestriction, setOf("wifi")) fun libraryUpdateRestriction() = prefs.getStringSet(Keys.libraryUpdateRestriction, setOf("wifi"))
fun libraryUpdateCategories() = flowPrefs.getStringSet(Keys.libraryUpdateCategories, emptySet()) fun libraryUpdateCategories() = flowPrefs.getStringSet(Keys.libraryUpdateCategories, emptySet())
fun libraryUpdateCategoriesExclude() = flowPrefs.getStringSet(Keys.libraryUpdateCategoriesExclude, emptySet())
fun libraryUpdatePrioritization() = flowPrefs.getInt(Keys.libraryUpdatePrioritization, 0) fun libraryUpdatePrioritization() = flowPrefs.getInt(Keys.libraryUpdatePrioritization, 0)

View File

@ -4,10 +4,10 @@ import android.app.Dialog
import android.os.Bundle import android.os.Bundle
import android.os.Handler import android.os.Handler
import android.view.View import android.view.View
import androidx.core.text.buildSpannedString
import androidx.preference.PreferenceScreen import androidx.preference.PreferenceScreen
import com.afollestad.materialdialogs.MaterialDialog import com.afollestad.materialdialogs.MaterialDialog
import com.afollestad.materialdialogs.customview.customView import com.afollestad.materialdialogs.customview.customView
import com.afollestad.materialdialogs.list.listItemsMultiChoice
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.DatabaseHelper import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.data.database.models.Category 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.switchPreference
import eu.kanade.tachiyomi.util.preference.titleRes import eu.kanade.tachiyomi.util.preference.titleRes
import eu.kanade.tachiyomi.widget.MinMaxNumberPicker 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.combine
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
@ -174,18 +176,37 @@ class SettingsLibraryController : SettingsController() {
LibraryGlobalUpdateCategoriesDialog().showDialog(router) LibraryGlobalUpdateCategoriesDialog().showDialog(router)
} }
preferences.libraryUpdateCategories().asFlow() fun updateSummary() {
.onEach { mutableSet -> val selectedCategories = preferences.libraryUpdateCategories().get()
val selectedCategories = mutableSet
.mapNotNull { id -> categories.find { it.id == id.toInt() } } .mapNotNull { id -> categories.find { it.id == id.toInt() } }
.sortedBy { it.order } .sortedBy { it.order }
val includedItemsText = if (selectedCategories.isEmpty()) {
summary = if (selectedCategories.isEmpty()) {
context.getString(R.string.all) context.getString(R.string.all)
} else { } else {
selectedCategories.joinToString { it.name } 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) .launchIn(viewScope)
} }
intListPreference { intListPreference {
@ -281,19 +302,34 @@ class SettingsLibraryController : SettingsController() {
val items = categories.map { it.name } val items = categories.map { it.name }
val preselected = categories val preselected = categories
.filter { it.id.toString() in preferences.libraryUpdateCategories().get() } .map {
.map { categories.indexOf(it) } 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() .toIntArray()
return MaterialDialog(activity!!) return MaterialDialog(activity!!)
.title(R.string.pref_library_update_categories) .title(R.string.pref_library_update_categories)
.listItemsMultiChoice( .listItemsQuadStateMultiChoice(
items = items, items = items,
initialSelection = preselected, initialSelected = preselected
allowEmptySelection = true ) { selections ->
) { _, selections, _ -> val included = selections
val newCategories = selections.map { categories[it] } .mapIndexed { index, value -> if (value == QuadStateCheckBox.State.CHECKED.ordinal) index else null }
preferences.libraryUpdateCategories().set(newCategories.map { it.id.toString() }.toSet()) .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) .positiveButton(android.R.string.ok)
.negativeButton(android.R.string.cancel) .negativeButton(android.R.string.cancel)

View File

@ -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<CharSequence>,
disabledIndices: IntArray? = null,
initialSelected: IntArray = IntArray(items.size),
selection: QuadStateMultiChoiceListener
): MaterialDialog {
return customListAdapter(
QuadStateMultiChoiceDialogAdapter(
dialog = this,
items = items,
disabledItems = disabledIndices,
initialSelected = initialSelected,
selection = selection
)
)
}

View File

@ -1,4 +1,4 @@
package eu.kanade.tachiyomi.widget package eu.kanade.tachiyomi.widget.materialdialogs
import android.content.Context import android.content.Context
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
@ -35,10 +35,11 @@ class QuadStateCheckBox @JvmOverloads constructor(context: Context, attrs: Attri
} }
} }
sealed class State { enum class State {
object UNCHECKED : State() UNCHECKED,
object INDETERMINATE : State() INDETERMINATE,
object CHECKED : State() CHECKED,
object INVERSED : State() INVERSED,
;
} }
} }

View File

@ -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<CharSequence>,
disabledItems: IntArray?,
initialSelected: IntArray,
internal var selection: QuadStateMultiChoiceListener
) : RecyclerView.Adapter<QuadStateMultiChoiceViewHolder>(),
DialogAdapter<CharSequence, QuadStateMultiChoiceListener> {
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<Any>
) {
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<CharSequence>,
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
}

View File

@ -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)
}

View File

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
style="@style/MD_ListItem.Choice">
<eu.kanade.tachiyomi.widget.materialdialogs.QuadStateCheckBox
android:id="@+id/md_quad_state_control"
style="@style/MD_ListItem_Control" />
<com.afollestad.materialdialogs.internal.rtl.RtlTextView
android:id="@+id/md_quad_state_title"
style="@style/MD_ListItemText.Choice"
tools:text="Item" />
</LinearLayout>

View File

@ -225,6 +225,9 @@
</plurals> </plurals>
<string name="pref_library_update_categories">Categories to include in global update</string> <string name="pref_library_update_categories">Categories to include in global update</string>
<string name="all">All</string> <string name="all">All</string>
<string name="none">None</string>
<string name="include">Include: %s</string>
<string name="exclude">Exclude: %s</string>
<!-- Extension section --> <!-- Extension section -->
<string name="all_lang">All</string> <string name="all_lang">All</string>