Эх сурвалжийг харах

Allow excluding categories from library update

Closes #3467, #4661, #1839

Supersedes #4474
arkon 4 жил өмнө
parent
commit
4f1275ac01

+ 10 - 1
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 }

+ 1 - 0
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"
 

+ 1 - 0
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)
 

+ 56 - 20
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)

+ 26 - 0
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<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
+        )
+    )
+}

+ 7 - 6
app/src/main/java/eu/kanade/tachiyomi/widget/QuadStateCheckBox.kt → 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,
+        ;
     }
 }

+ 187 - 0
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<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
+}

+ 28 - 0
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)
+}

+ 15 - 0
app/src/main/res/layout/md_listitem_quadstatemultichoice.xml

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

+ 3 - 0
app/src/main/res/values/strings.xml

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