Allow excluding categories from library update

closes #571

Co-Authored-By: arkon <4098258+arkon@users.noreply.github.com>
This commit is contained in:
Jays2Kings 2021-07-03 19:42:24 -04:00
parent 24583e4cc5
commit d5a387b056
13 changed files with 479 additions and 8 deletions

View File

@ -200,26 +200,35 @@ class LibraryUpdateService(
* @return a list of manga to update * @return a list of manga to update
*/ */
private fun getMangaToUpdate(categoryId: Int, target: Target): List<LibraryManga> { private fun getMangaToUpdate(categoryId: Int, target: Target): List<LibraryManga> {
val libraryManga = db.getLibraryMangas().executeAsBlocking()
var listToUpdate = if (categoryId != -1) { var listToUpdate = if (categoryId != -1) {
categoryIds.add(categoryId) categoryIds.add(categoryId)
db.getLibraryMangas().executeAsBlocking().filter { it.category == categoryId } libraryManga.filter { it.category == categoryId }
} else { } else {
val categoriesToUpdate = val categoriesToUpdate =
preferences.libraryUpdateCategories().get().map(String::toInt) preferences.libraryUpdateCategories().get().map(String::toInt)
if (categoriesToUpdate.isNotEmpty()) { if (categoriesToUpdate.isNotEmpty()) {
categoryIds.addAll(categoriesToUpdate) categoryIds.addAll(categoriesToUpdate)
db.getLibraryMangas().executeAsBlocking() libraryManga.filter { it.category in categoriesToUpdate }.distinctBy { it.id }
.filter { it.category in categoriesToUpdate }.distinctBy { it.id }
} else { } else {
categoryIds.addAll(db.getCategories().executeAsBlocking().mapNotNull { it.id } + 0) 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()) { if (target == Target.CHAPTERS && preferences.updateOnlyNonCompleted()) {
listToUpdate = listToUpdate.filter { it.status != SManga.COMPLETED } 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<LibraryManga>, startId: Int) { private fun launchTarget(target: Target, mangaToAdd: List<LibraryManga>, startId: Int) {

View File

@ -127,6 +127,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

@ -255,6 +255,7 @@ class PreferencesHelper(val context: Context) {
fun libraryUpdateRestriction() = prefs.getStringSet(Keys.libraryUpdateRestriction, emptySet()) fun libraryUpdateRestriction() = prefs.getStringSet(Keys.libraryUpdateRestriction, emptySet())
fun libraryUpdateCategories() = flowPrefs.getStringSet(Keys.libraryUpdateCategories, emptySet()) fun libraryUpdateCategories() = flowPrefs.getStringSet(Keys.libraryUpdateCategories, emptySet())
fun libraryUpdateCategoriesExclude() = flowPrefs.getStringSet(Keys.libraryUpdateCategoriesExclude, emptySet())
fun libraryUpdatePrioritization() = rxPrefs.getInteger(Keys.libraryUpdatePrioritization, 0) fun libraryUpdatePrioritization() = rxPrefs.getInteger(Keys.libraryUpdatePrioritization, 0)

View File

@ -19,6 +19,7 @@ import eu.kanade.tachiyomi.util.system.getResourceColor
import eu.kanade.tachiyomi.widget.preference.IntListMatPreference import eu.kanade.tachiyomi.widget.preference.IntListMatPreference
import eu.kanade.tachiyomi.widget.preference.ListMatPreference import eu.kanade.tachiyomi.widget.preference.ListMatPreference
import eu.kanade.tachiyomi.widget.preference.MultiListMatPreference import eu.kanade.tachiyomi.widget.preference.MultiListMatPreference
import eu.kanade.tachiyomi.widget.preference.TriStateListPreference
@DslMarker @DslMarker
@Target(AnnotationTarget.TYPE) @Target(AnnotationTarget.TYPE)
@ -84,6 +85,17 @@ inline fun PreferenceGroup.multiSelectListPreferenceMat(
return initThenAdd(MultiListMatPreference(activity, context), block) 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 { inline fun PreferenceScreen.preferenceCategory(block: (@DSL PreferenceCategory).() -> Unit): PreferenceCategory {
return addThenInit( return addThenInit(
PreferenceCategory(context).apply { PreferenceCategory(context).apply {

View File

@ -97,7 +97,7 @@ class SettingsLibraryController : SettingsController() {
} }
preferenceCategory { preferenceCategory {
titleRes = R.string.updates titleRes = R.string.global_updates
intListPreference(activity) { intListPreference(activity) {
key = Keys.libraryUpdateInterval key = Keys.libraryUpdateInterval
titleRes = R.string.library_update_frequency titleRes = R.string.library_update_frequency
@ -161,9 +161,10 @@ class SettingsLibraryController : SettingsController() {
summaryRes = R.string.select_order_to_update summaryRes = R.string.select_order_to_update
} }
multiSelectListPreferenceMat(activity) { triStateListPreference(activity) {
key = Keys.libraryUpdateCategories 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 val categories = listOf(Category.createDefault(context)) + dbCategories
entries = categories.map { it.name } entries = categories.map { it.name }

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

View File

@ -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,
;
}
}

View File

@ -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<CharSequence>) -> Unit)?
internal class QuadStateMultiChoiceDialogAdapter(
private var dialog: MaterialDialog,
internal var items: List<CharSequence>,
disabledItems: IntArray?,
initialSelection: IntArray,
internal var selection: QuadStateMultiChoiceListener
) : RecyclerView.Adapter<QuadStateMultiChoiceViewHolder>(),
DialogAdapter<CharSequence, QuadStateMultiChoiceListener> {
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 <reified T> List<T>.pullIndices(indices: IntArray): List<T> {
return mutableListOf<T>().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<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,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)
}

View File

@ -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<String> = emptySet()
override fun onSetInitialValue(defaultValue: Any?) {
super.onSetInitialValue(defaultValue)
defValue = (defaultValue as? Collection<*>).orEmpty().mapNotNull { it as? String }.toSet()
}
override var customSummaryProvider: SummaryProvider<MatPreference>? = SummaryProvider<MatPreference> {
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)
}
}
}
}

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/black"
android:pathData="M19,3H5C3.9,3 3,3.9 3,5v14c0,1.1 0.9,2 2,2h14c1.1,0 2,-0.9 2,-2V5C21,3.9 20.1,3 19,3zM17,13H7v-2h10V13z" />
</vector>

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

@ -216,6 +216,7 @@
<string name="auto_refresh_covers">Automatically refresh covers</string> <string name="auto_refresh_covers">Automatically refresh covers</string>
<string name="auto_refresh_covers_summary">Refresh covers in library as well <string name="auto_refresh_covers_summary">Refresh covers in library as well
when updating library</string> when updating library</string>
<string name="global_updates">Global updates</string>
<string name="show_notification_error">Show a notification for errors</string> <string name="show_notification_error">Show a notification for errors</string>
<string name="display_buttons_bottom_reader">Buttons at bottom of reader</string> <string name="display_buttons_bottom_reader">Buttons at bottom of reader</string>
<string name="certain_buttons_can_be_found">Certain buttons can be found in other places if disabled here</string> <string name="certain_buttons_can_be_found">Certain buttons can be found in other places if disabled here</string>
@ -896,12 +897,14 @@
<string name="edit">Edit</string> <string name="edit">Edit</string>
<string name="enable">Enable</string> <string name="enable">Enable</string>
<string name="enabled">Enabled</string> <string name="enabled">Enabled</string>
<string name="exclude_">Exclude: %s</string>
<string name="fast">Fast</string> <string name="fast">Fast</string>
<string name="filter">Filter</string> <string name="filter">Filter</string>
<string name="forward">Forward</string> <string name="forward">Forward</string>
<string name="free">Free</string> <string name="free">Free</string>
<string name="hide">Hide</string> <string name="hide">Hide</string>
<string name="ignore">Ignore</string> <string name="ignore">Ignore</string>
<string name="include_">Include: %s</string>
<string name="install">Install</string> <string name="install">Install</string>
<string name="keep">Keep</string> <string name="keep">Keep</string>
<string name="left">Left</string> <string name="left">Left</string>