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 d5c856e36e..410ffc12c9 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 @@ -200,26 +200,35 @@ class LibraryUpdateService( * @return a list of manga to update */ private fun getMangaToUpdate(categoryId: Int, target: Target): List { + val libraryManga = db.getLibraryMangas().executeAsBlocking() + var listToUpdate = if (categoryId != -1) { categoryIds.add(categoryId) - db.getLibraryMangas().executeAsBlocking().filter { it.category == categoryId } + libraryManga.filter { it.category == categoryId } } else { val categoriesToUpdate = preferences.libraryUpdateCategories().get().map(String::toInt) if (categoriesToUpdate.isNotEmpty()) { categoryIds.addAll(categoriesToUpdate) - db.getLibraryMangas().executeAsBlocking() - .filter { it.category in categoriesToUpdate }.distinctBy { it.id } + libraryManga.filter { it.category in categoriesToUpdate }.distinctBy { it.id } } else { categoryIds.addAll(db.getCategories().executeAsBlocking().mapNotNull { it.id } + 0) - db.getLibraryMangas().executeAsBlocking().distinctBy { it.id } + libraryManga.distinctBy { it.id } } } if (target == Target.CHAPTERS && preferences.updateOnlyNonCompleted()) { listToUpdate = listToUpdate.filter { it.status != SManga.COMPLETED } } - return listToUpdate + val categoriesToExclude = + preferences.libraryUpdateCategoriesExclude().get().map(String::toInt) + val listToExclude = if (categoriesToExclude.isNotEmpty()) { + libraryManga.filter { it.category in categoriesToExclude } + } else { + emptyList() + } + + return listToUpdate.minus(listToExclude) } private fun launchTarget(target: Target, mangaToAdd: List, startId: Int) { 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 f2d3a9fd82..1c897a11f9 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 @@ -127,6 +127,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 d01a5fab74..4a7a77afe6 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 @@ -255,6 +255,7 @@ class PreferencesHelper(val context: Context) { fun libraryUpdateRestriction() = prefs.getStringSet(Keys.libraryUpdateRestriction, emptySet()) fun libraryUpdateCategories() = flowPrefs.getStringSet(Keys.libraryUpdateCategories, emptySet()) + fun libraryUpdateCategoriesExclude() = flowPrefs.getStringSet(Keys.libraryUpdateCategoriesExclude, emptySet()) fun libraryUpdatePrioritization() = rxPrefs.getInteger(Keys.libraryUpdatePrioritization, 0) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/PreferenceDSL.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/PreferenceDSL.kt index beb97a6644..3436225e3c 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/PreferenceDSL.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/PreferenceDSL.kt @@ -19,6 +19,7 @@ import eu.kanade.tachiyomi.util.system.getResourceColor import eu.kanade.tachiyomi.widget.preference.IntListMatPreference import eu.kanade.tachiyomi.widget.preference.ListMatPreference import eu.kanade.tachiyomi.widget.preference.MultiListMatPreference +import eu.kanade.tachiyomi.widget.preference.TriStateListPreference @DslMarker @Target(AnnotationTarget.TYPE) @@ -84,6 +85,17 @@ inline fun PreferenceGroup.multiSelectListPreferenceMat( return initThenAdd(MultiListMatPreference(activity, context), block) } +inline fun PreferenceGroup.triStateListPreference( + activity: Activity?, + block: ( + @DSL + TriStateListPreference + ).() + -> Unit +): TriStateListPreference { + return initThenAdd(TriStateListPreference(activity, context), block) +} + inline fun PreferenceScreen.preferenceCategory(block: (@DSL PreferenceCategory).() -> Unit): PreferenceCategory { return addThenInit( PreferenceCategory(context).apply { 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 abfc99e110..dd57eda4dd 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 @@ -97,7 +97,7 @@ class SettingsLibraryController : SettingsController() { } preferenceCategory { - titleRes = R.string.updates + titleRes = R.string.global_updates intListPreference(activity) { key = Keys.libraryUpdateInterval titleRes = R.string.library_update_frequency @@ -161,9 +161,10 @@ class SettingsLibraryController : SettingsController() { summaryRes = R.string.select_order_to_update } - multiSelectListPreferenceMat(activity) { + triStateListPreference(activity) { key = Keys.libraryUpdateCategories - titleRes = R.string.categories_to_include_in_global_update + excludeKey = Keys.libraryUpdateCategoriesExclude + titleRes = R.string.categories val categories = listOf(Category.createDefault(context)) + dbCategories entries = categories.map { it.name } 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..3201145a5f --- /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, + initialSelection: IntArray = IntArray(items.size), + selection: QuadStateMultiChoiceListener +): MaterialDialog { + return customListAdapter( + QuadStateMultiChoiceDialogAdapter( + dialog = this, + items = items, + disabledItems = disabledIndices, + initialSelection = initialSelection, + selection = selection + ) + ) +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/widget/materialdialogs/QuadStateCheckBox.kt b/app/src/main/java/eu/kanade/tachiyomi/widget/materialdialogs/QuadStateCheckBox.kt new file mode 100644 index 0000000000..b01d224374 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/widget/materialdialogs/QuadStateCheckBox.kt @@ -0,0 +1,34 @@ +package eu.kanade.tachiyomi.widget.materialdialogs + +import android.content.Context +import android.util.AttributeSet +import androidx.appcompat.widget.AppCompatImageView +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.util.view.setVectorCompat + +class QuadStateCheckBox @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) : + AppCompatImageView(context, attrs) { + + var state: State = State.UNCHECKED + set(value) { + field = value + updateDrawable() + } + + private fun updateDrawable() { + when (state) { + State.UNCHECKED -> setVectorCompat(R.drawable.ic_check_box_outline_blank_24dp, R.attr.colorControlNormal) + State.INDETERMINATE -> setVectorCompat(R.drawable.ic_indeterminate_check_box_24dp, R.attr.colorAccent) + State.CHECKED -> setVectorCompat(R.drawable.ic_check_box_24dp, R.attr.colorAccent) + State.INVERSED -> setVectorCompat(R.drawable.ic_check_box_x_24dp, R.attr.colorAccent) + } + } + + 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..f156c23e8f --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/widget/materialdialogs/QuadStateMultiChoiceDialogAdapter.kt @@ -0,0 +1,197 @@ +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 = ((dialog: MaterialDialog, indices: IntArray, items: List) -> Unit)? + +internal class QuadStateMultiChoiceDialogAdapter( + private var dialog: MaterialDialog, + internal var items: List, + disabledItems: IntArray?, + initialSelection: IntArray, + internal var selection: QuadStateMultiChoiceListener +) : RecyclerView.Adapter(), + DialogAdapter { + + private val states = QuadStateCheckBox.State.values() + + private var currentSelection: IntArray = initialSelection + 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 + } + currentSelection = newSelection.toIntArray() + val selectedItems = this.items.pullIndices(this.currentSelection) + selection?.invoke(dialog, currentSelection, selectedItems) + } + + internal inline fun List.pullIndices(indices: IntArray): List { + return mutableListOf().apply { + for (index in indices) { + add(this@pullIndices[index]) + } + } + } + + 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..27f0133b51 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/widget/materialdialogs/QuadStateMultiChoiceViewHolder.kt @@ -0,0 +1,30 @@ +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 + titleView.alpha = if (value) 1f else 0.75f + controlView.alpha = if (value) 1f else 0.75f + } + + override fun onClick(view: View) = adapter.itemClicked(bindingAdapterPosition) +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/widget/preference/TriStateListPreference.kt b/app/src/main/java/eu/kanade/tachiyomi/widget/preference/TriStateListPreference.kt new file mode 100644 index 0000000000..c64ea965a8 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/widget/preference/TriStateListPreference.kt @@ -0,0 +1,133 @@ +package eu.kanade.tachiyomi.widget.preference + +import android.annotation.SuppressLint +import android.app.Activity +import android.content.Context +import android.util.AttributeSet +import androidx.core.text.buildSpannedString +import com.afollestad.materialdialogs.MaterialDialog +import com.afollestad.materialdialogs.list.checkItem +import com.afollestad.materialdialogs.list.uncheckItem +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.data.preference.getOrDefault +import eu.kanade.tachiyomi.widget.materialdialogs.QuadStateCheckBox +import eu.kanade.tachiyomi.widget.materialdialogs.listItemsQuadStateMultiChoice + +class TriStateListPreference @JvmOverloads constructor( + activity: Activity?, + context: Context, + attrs: AttributeSet? = + null +) : + ListMatPreference(activity, context, attrs) { + + var allSelectionRes: Int? = null + var excludeKey: String? = null + + /** All item is always selected and uncheckabele */ + var allIsAlwaysSelected = false + set(value) { + field = value + notifyChanged() + } + + /** All Item is moved to bottom of list if true */ + var showAllLast = false + set(value) { + field = value + notifyChanged() + } + + private var defValue: Set = emptySet() + + override fun onSetInitialValue(defaultValue: Any?) { + super.onSetInitialValue(defaultValue) + defValue = (defaultValue as? Collection<*>).orEmpty().mapNotNull { it as? String }.toSet() + } + + override var customSummaryProvider: SummaryProvider? = SummaryProvider { + var includedStrings = prefs.getStringSet(key, defValue).getOrDefault().mapNotNull { value -> + entryValues.indexOf(value).takeUnless { it == -1 } + }.toIntArray().sorted().map { entries[it] } + allSelectionRes?.let { allRes -> + when { + includedStrings.isEmpty() -> includedStrings = listOf(context.getString(allRes)) + allIsAlwaysSelected && !showAllLast -> + includedStrings = + listOf(context.getString(allRes)) + includedStrings + allIsAlwaysSelected -> includedStrings = includedStrings + context.getString(allRes) + } + } + val excludedStrings = excludeKey?.let { + prefs.getStringSet(it, defValue).getOrDefault().mapNotNull { value -> + entryValues.indexOf(value).takeUnless { + it == -1 + } + } + }?.toIntArray()?.sorted()?.map { entries[it] }?.takeIf { it.isNotEmpty() } + ?: listOf(context.getString(R.string.none)) + buildSpannedString { + append(context.getString(R.string.include_, includedStrings.joinToString())) + appendLine() + append(context.getString(R.string.exclude_, excludedStrings.joinToString())) + } + } + + @SuppressLint("CheckResult") + override fun MaterialDialog.setItems() { + val set = prefs.getStringSet(key, defValue).getOrDefault() + val items = if (allSelectionRes != null) { + if (showAllLast) entries + listOf(context.getString(allSelectionRes!!)) + else listOf(context.getString(allSelectionRes!!)) + entries + } else entries + val allPos = if (showAllLast) items.size - 1 else 0 + val excludedSet = excludeKey?.let { + prefs.getStringSet(it, defValue).getOrDefault() + }.orEmpty() + val allValue = intArrayOf( + if (set.isEmpty()) QuadStateCheckBox.State.CHECKED.ordinal + else QuadStateCheckBox.State.UNCHECKED.ordinal + ) + val preselected = + if (allSelectionRes != null && !showAllLast) { allValue } else { intArrayOf() } + entryValues + .map { + when (it) { + in set -> QuadStateCheckBox.State.CHECKED.ordinal + in excludedSet -> QuadStateCheckBox.State.INVERSED.ordinal + else -> QuadStateCheckBox.State.UNCHECKED.ordinal + } + } + .toIntArray() + + if (allSelectionRes != null && showAllLast) { allValue } else { intArrayOf() } + var includedItems = set + var excludedItems = excludedSet + positiveButton(android.R.string.ok) { + prefs.getStringSet(key, emptySet()).set(includedItems) + excludeKey?.let { prefs.getStringSet(it, emptySet()).set(excludedItems) } + callChangeListener(includedItems to excludedItems) + notifyChanged() + } + listItemsQuadStateMultiChoice( + items = items, + disabledIndices = if (allSelectionRes != null) intArrayOf(allPos) else null, + initialSelection = preselected + ) { _, sels, _ -> + val selections = sels.filterIndexed { index, i -> allSelectionRes == null || index != allPos } + includedItems = selections + .mapIndexed { index, value -> if (value == QuadStateCheckBox.State.CHECKED.ordinal) index else null } + .filterNotNull() + .map { entryValues[it] } + .toSet() + excludedItems = selections + .mapIndexed { index, value -> if (value == QuadStateCheckBox.State.INVERSED.ordinal) index else null } + .filterNotNull() + .map { entryValues[it] } + .toSet() + + if (allSelectionRes != null && !allIsAlwaysSelected) { + if (includedItems.isEmpty()) checkItem(allPos) + else uncheckItem(allPos) + } + } + } +} diff --git a/app/src/main/res/drawable/ic_indeterminate_check_box_24dp.xml b/app/src/main/res/drawable/ic_indeterminate_check_box_24dp.xml new file mode 100644 index 0000000000..1f089d1d7d --- /dev/null +++ b/app/src/main/res/drawable/ic_indeterminate_check_box_24dp.xml @@ -0,0 +1,9 @@ + + + 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 b1ea9625a0..c1317a4bdb 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -216,6 +216,7 @@ Automatically refresh covers Refresh covers in library as well when updating library + Global updates Show a notification for errors Buttons at bottom of reader Certain buttons can be found in other places if disabled here @@ -896,12 +897,14 @@ Edit Enable Enabled + Exclude: %s Fast Filter Forward Free Hide Ignore + Include: %s Install Keep Left