Allow excluding categories from library update
Closes #3467, #4661, #1839 Supersedes #4474
This commit is contained in:
parent
b2fee7035f
commit
4f1275ac01
10 changed files with 334 additions and 27 deletions
|
@ -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 }
|
||||
|
|
|
@ -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"
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
)
|
||||
)
|
||||
}
|
|
@ -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,
|
||||
;
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
15
app/src/main/res/layout/md_listitem_quadstatemultichoice.xml
Normal file
15
app/src/main/res/layout/md_listitem_quadstatemultichoice.xml
Normal 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>
|
|
@ -225,6 +225,9 @@
|
|||
</plurals>
|
||||
<string name="pref_library_update_categories">Categories to include in global update</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 -->
|
||||
<string name="all_lang">All</string>
|
||||
|
|
Reference in a new issue