mirror of
				https://github.com/mihonapp/mihon.git
				synced 2025-10-31 14:27:57 +01:00 
			
		
		
		
	Allow excluding categories from library update
Closes #3467, #4661, #1839 Supersedes #4474
This commit is contained in:
		| @@ -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 New Issue
	
	Block a user