Edit mangas' Categories in Library using TriState list (#5422)

* Use QuadState Categories to edit mangas in Library

Add updateMangasToCategories to build build correct Categories list for
  each manga using Common and Mix list
Update QuadState Multi-Choice to either Action or Display List
  Display list would have different state sequece from Action
  Uncheck-> Indeterminate (only if initial so)-> Check

fixup manga categories logic as Windows and push request comments

* fixup: Use QuadStateTextView.State enum

Update function to use  QuadStateTextView.State enum that missed in last change

* fixup: missing closing bracket and type cast

Co-authored-by: quangkieu <quangkieu1993@gmail.com>
This commit is contained in:
Quang Kieu 2021-09-04 11:13:19 -04:00 committed by GitHub
parent c316e7faab
commit ee711dc0fb
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 121 additions and 31 deletions

View file

@ -49,6 +49,7 @@ import eu.kanade.tachiyomi.util.view.shrinkOnScroll
import eu.kanade.tachiyomi.util.view.snack import eu.kanade.tachiyomi.util.view.snack
import eu.kanade.tachiyomi.widget.AutofitRecyclerView import eu.kanade.tachiyomi.widget.AutofitRecyclerView
import eu.kanade.tachiyomi.widget.EmptyView import eu.kanade.tachiyomi.widget.EmptyView
import eu.kanade.tachiyomi.widget.materialdialogs.QuadStateTextView
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.drop import kotlinx.coroutines.flow.drop
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
@ -626,8 +627,12 @@ open class BrowseSourceController(bundle: Bundle) :
// Choose a category // Choose a category
else -> { else -> {
val ids = presenter.getMangaCategoryIds(manga) val ids = presenter.getMangaCategoryIds(manga)
val preselected = ids.mapNotNull { id -> val preselected = categories.map {
categories.indexOfFirst { it.id == id }.takeIf { it != -1 } if (it.id in ids) {
QuadStateTextView.State.CHECKED.ordinal
} else {
QuadStateTextView.State.UNCHECKED.ordinal
}
}.toTypedArray() }.toTypedArray()
ChangeMangaCategoriesDialog(this, listOf(manga), categories, preselected) ChangeMangaCategoriesDialog(this, listOf(manga), categories, preselected)
@ -643,11 +648,11 @@ open class BrowseSourceController(bundle: Bundle) :
* @param mangas The list of manga to move to categories. * @param mangas The list of manga to move to categories.
* @param categories The list of categories where manga will be placed. * @param categories The list of categories where manga will be placed.
*/ */
override fun updateCategoriesForMangas(mangas: List<Manga>, categories: List<Category>) { override fun updateCategoriesForMangas(mangas: List<Manga>, addCategories: List<Category>, removeCategories: List<Category>) {
val manga = mangas.firstOrNull() ?: return val manga = mangas.firstOrNull() ?: return
presenter.changeMangaFavorite(manga) presenter.changeMangaFavorite(manga)
presenter.updateMangaCategories(manga, categories) presenter.updateMangaCategories(manga, addCategories)
val position = adapter?.currentItems?.indexOfFirst { it -> (it as SourceItem).manga.id == manga.id } val position = adapter?.currentItems?.indexOfFirst { it -> (it as SourceItem).manga.id == manga.id }
if (position != null) { if (position != null) {

View file

@ -10,6 +10,8 @@ import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.ui.base.controller.DialogController import eu.kanade.tachiyomi.ui.base.controller.DialogController
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
import eu.kanade.tachiyomi.ui.category.CategoryController import eu.kanade.tachiyomi.ui.category.CategoryController
import eu.kanade.tachiyomi.widget.materialdialogs.QuadStateTextView
import eu.kanade.tachiyomi.widget.materialdialogs.setQuadStateMultiChoiceItems
class ChangeMangaCategoriesDialog<T>(bundle: Bundle? = null) : class ChangeMangaCategoriesDialog<T>(bundle: Bundle? = null) :
DialogController(bundle) where T : Controller, T : ChangeMangaCategoriesDialog.Listener { DialogController(bundle) where T : Controller, T : ChangeMangaCategoriesDialog.Listener {
@ -17,6 +19,7 @@ class ChangeMangaCategoriesDialog<T>(bundle: Bundle? = null) :
private var mangas = emptyList<Manga>() private var mangas = emptyList<Manga>()
private var categories = emptyList<Category>() private var categories = emptyList<Category>()
private var preselected = emptyArray<Int>() private var preselected = emptyArray<Int>()
private var selected = emptyArray<Int>().toIntArray()
constructor( constructor(
target: T, target: T,
@ -27,6 +30,7 @@ class ChangeMangaCategoriesDialog<T>(bundle: Bundle? = null) :
this.mangas = mangas this.mangas = mangas
this.categories = categories this.categories = categories
this.preselected = preselected this.preselected = preselected
this.selected = preselected.toIntArray()
targetController = target targetController = target
} }
@ -36,15 +40,21 @@ class ChangeMangaCategoriesDialog<T>(bundle: Bundle? = null) :
.setNegativeButton(android.R.string.cancel, null) .setNegativeButton(android.R.string.cancel, null)
.apply { .apply {
if (categories.isNotEmpty()) { if (categories.isNotEmpty()) {
val selected = categories setQuadStateMultiChoiceItems(
.mapIndexed { i, _ -> preselected.contains(i) } items = categories.map { it.name },
.toBooleanArray() isActionList = false,
setMultiChoiceItems(categories.map { it.name }.toTypedArray(), selected) { _, which, checked -> initialSelected = preselected.toIntArray()
selected[which] = checked ) { selections ->
selected = selections
} }
setPositiveButton(android.R.string.ok) { _, _ -> setPositiveButton(android.R.string.ok) { _, _ ->
val newCategories = categories.filterIndexed { i, _ -> selected[i] } val add = selected
(targetController as? Listener)?.updateCategoriesForMangas(mangas, newCategories) .mapIndexed { index, value -> if (value == QuadStateTextView.State.CHECKED.ordinal) categories[index] else null }
.filterNotNull()
val remove = selected
.mapIndexed { index, value -> if (value == QuadStateTextView.State.UNCHECKED.ordinal) categories[index] else null }
.filterNotNull()
(targetController as? Listener)?.updateCategoriesForMangas(mangas, add, remove)
} }
} else { } else {
setMessage(R.string.information_empty_category_dialog) setMessage(R.string.information_empty_category_dialog)
@ -62,6 +72,6 @@ class ChangeMangaCategoriesDialog<T>(bundle: Bundle? = null) :
} }
interface Listener { interface Listener {
fun updateCategoriesForMangas(mangas: List<Manga>, categories: List<Category>) fun updateCategoriesForMangas(mangas: List<Manga>, addCategories: List<Category>, removeCategories: List<Category> = emptyList<Category>())
} }
} }

View file

@ -36,6 +36,7 @@ import eu.kanade.tachiyomi.util.system.getResourceColor
import eu.kanade.tachiyomi.util.system.openInBrowser import eu.kanade.tachiyomi.util.system.openInBrowser
import eu.kanade.tachiyomi.util.system.toast import eu.kanade.tachiyomi.util.system.toast
import eu.kanade.tachiyomi.widget.EmptyView import eu.kanade.tachiyomi.widget.EmptyView
import eu.kanade.tachiyomi.widget.materialdialogs.QuadStateTextView
import kotlinx.coroutines.flow.drop import kotlinx.coroutines.flow.drop
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
@ -558,11 +559,17 @@ class LibraryController(
val categories = presenter.categories.filter { it.id != 0 } val categories = presenter.categories.filter { it.id != 0 }
// Get indexes of the common categories to preselect. // Get indexes of the common categories to preselect.
val commonCategoriesIndexes = presenter.getCommonCategories(mangas) val common = presenter.getCommonCategories(mangas)
.map { categories.indexOf(it) } // Get indexes of the mix categories to preselect.
.toTypedArray() val mix = presenter.getMixCategories(mangas)
var preselected = categories.map {
ChangeMangaCategoriesDialog(this, mangas, categories, commonCategoriesIndexes) when (it) {
in common -> QuadStateTextView.State.CHECKED.ordinal
in mix -> QuadStateTextView.State.INDETERMINATE.ordinal
else -> QuadStateTextView.State.UNCHECKED.ordinal
}
}.toTypedArray()
ChangeMangaCategoriesDialog(this, mangas, categories, preselected)
.showDialog(router) .showDialog(router)
} }
@ -582,8 +589,8 @@ class LibraryController(
DeleteLibraryMangasDialog(this, selectedMangas.toList()).showDialog(router) DeleteLibraryMangasDialog(this, selectedMangas.toList()).showDialog(router)
} }
override fun updateCategoriesForMangas(mangas: List<Manga>, categories: List<Category>) { override fun updateCategoriesForMangas(mangas: List<Manga>, addCategories: List<Category>, removeCategories: List<Category>) {
presenter.moveMangasToCategories(categories, mangas) presenter.updateMangasToCategories(mangas, addCategories, removeCategories)
destroyActionModeIfNeeded() destroyActionModeIfNeeded()
} }

View file

@ -442,6 +442,18 @@ class LibraryPresenter(
.reduce { set1: Iterable<Category>, set2 -> set1.intersect(set2).toMutableList() } .reduce { set1: Iterable<Category>, set2 -> set1.intersect(set2).toMutableList() }
} }
/**
* Returns the mix (non-common) categories for the given list of manga.
*
* @param mangas the list of manga.
*/
fun getMixCategories(mangas: List<Manga>): Collection<Category> {
if (mangas.isEmpty()) return emptyList()
val mangaCategories = mangas.toSet().map { db.getCategoriesForManga(it).executeAsBlocking() }
val common = mangaCategories.reduce { set1, set2 -> set1.intersect(set2).toMutableList() }
return mangaCategories.flatten().distinct().subtract(common).toMutableList()
}
/** /**
* Queues all unread chapters from the given list of manga. * Queues all unread chapters from the given list of manga.
* *
@ -533,4 +545,21 @@ class LibraryPresenter(
db.setMangaCategories(mc, mangas) db.setMangaCategories(mc, mangas)
} }
/**
* Bulk update categories of mangas using old and new common categories.
*
* @param mangas the list of manga to move.
* @param addCategories the categories to add for all mangas.
* @param removeCategories the categories to remove in all mangas.
*/
fun updateMangasToCategories(mangas: List<Manga>, addCategories: List<Category>, removeCategories: List<Category>) {
val mangaCategories = mangas.map { manga ->
val categories = db.getCategoriesForManga(manga).executeAsBlocking()
.subtract(removeCategories).plus(addCategories).distinct()
categories.map { MangaCategory.create(manga, it) }
}.flatten()
db.setMangaCategories(mangaCategories, mangas)
}
} }

View file

@ -93,6 +93,7 @@ import eu.kanade.tachiyomi.util.system.toast
import eu.kanade.tachiyomi.util.view.getCoordinates import eu.kanade.tachiyomi.util.view.getCoordinates
import eu.kanade.tachiyomi.util.view.shrinkOnScroll import eu.kanade.tachiyomi.util.view.shrinkOnScroll
import eu.kanade.tachiyomi.util.view.snack import eu.kanade.tachiyomi.util.view.snack
import eu.kanade.tachiyomi.widget.materialdialogs.QuadStateTextView
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import reactivecircus.flowbinding.recyclerview.scrollEvents import reactivecircus.flowbinding.recyclerview.scrollEvents
@ -578,8 +579,12 @@ class MangaController :
// Choose a category // Choose a category
else -> { else -> {
val ids = presenter.getMangaCategoryIds(manga) val ids = presenter.getMangaCategoryIds(manga)
val preselected = ids.mapNotNull { id -> val preselected = categories.map {
categories.indexOfFirst { it.id == id }.takeIf { it != -1 } if (it.id in ids) {
QuadStateTextView.State.CHECKED.ordinal
} else {
QuadStateTextView.State.UNCHECKED.ordinal
}
}.toTypedArray() }.toTypedArray()
ChangeMangaCategoriesDialog(this, listOf(manga), categories, preselected) ChangeMangaCategoriesDialog(this, listOf(manga), categories, preselected)
@ -627,15 +632,18 @@ class MangaController :
val categories = presenter.getCategories() val categories = presenter.getCategories()
val ids = presenter.getMangaCategoryIds(manga) val ids = presenter.getMangaCategoryIds(manga)
val preselected = ids.mapNotNull { id -> val preselected = categories.map {
categories.indexOfFirst { it.id == id }.takeIf { it != -1 } if (it.id in ids) {
QuadStateTextView.State.CHECKED.ordinal
} else {
QuadStateTextView.State.UNCHECKED.ordinal
}
}.toTypedArray() }.toTypedArray()
ChangeMangaCategoriesDialog(this, listOf(manga), categories, preselected) ChangeMangaCategoriesDialog(this, listOf(manga), categories, preselected)
.showDialog(router) .showDialog(router)
} }
override fun updateCategoriesForMangas(mangas: List<Manga>, categories: List<Category>) { override fun updateCategoriesForMangas(mangas: List<Manga>, addCategories: List<Category>, removeCategories: List<Category>) {
val manga = mangas.firstOrNull() ?: return val manga = mangas.firstOrNull() ?: return
if (!manga.favorite) { if (!manga.favorite) {
@ -644,7 +652,7 @@ class MangaController :
activity?.invalidateOptionsMenu() activity?.invalidateOptionsMenu()
} }
presenter.moveMangaToCategories(manga, categories) presenter.moveMangaToCategories(manga, addCategories)
} }
/** /**

View file

@ -39,6 +39,7 @@ fun MaterialAlertDialogBuilder.setTextInput(
*/ */
fun MaterialAlertDialogBuilder.setQuadStateMultiChoiceItems( fun MaterialAlertDialogBuilder.setQuadStateMultiChoiceItems(
@StringRes message: Int? = null, @StringRes message: Int? = null,
isActionList: Boolean = true,
items: List<CharSequence>, items: List<CharSequence>,
initialSelected: IntArray, initialSelected: IntArray,
disabledIndices: IntArray? = null, disabledIndices: IntArray? = null,
@ -50,6 +51,7 @@ fun MaterialAlertDialogBuilder.setQuadStateMultiChoiceItems(
items = items, items = items,
disabledItems = disabledIndices, disabledItems = disabledIndices,
initialSelected = initialSelected, initialSelected = initialSelected,
isActionList = isActionList,
listener = selection listener = selection
) )
val updateScrollIndicators = { val updateScrollIndicators = {

View file

@ -8,14 +8,18 @@ import eu.kanade.tachiyomi.databinding.DialogQuadstatemultichoiceItemBinding
private object CheckPayload private object CheckPayload
private object InverseCheckPayload private object InverseCheckPayload
private object UncheckPayload private object UncheckPayload
private object IndeterminatePayload
typealias QuadStateMultiChoiceListener = (indices: IntArray) -> Unit typealias QuadStateMultiChoiceListener = (indices: IntArray) -> Unit
// isAction state: Uncheck-> Check-> Invert else Uncheck-> Indeterminate (only if initial so)-> Check
// isAction for list of action to operate on like filter include, exclude
internal class QuadStateMultiChoiceDialogAdapter( internal class QuadStateMultiChoiceDialogAdapter(
internal var items: List<CharSequence>, internal var items: List<CharSequence>,
disabledItems: IntArray?, disabledItems: IntArray?,
initialSelected: IntArray, private var initialSelected: IntArray,
internal var listener: QuadStateMultiChoiceListener internal var listener: QuadStateMultiChoiceListener,
val isActionList: Boolean = true
) : RecyclerView.Adapter<QuadStateMultiChoiceViewHolder>() { ) : RecyclerView.Adapter<QuadStateMultiChoiceViewHolder>() {
private val states = QuadStateTextView.State.values() private val states = QuadStateTextView.State.values()
@ -39,12 +43,15 @@ internal class QuadStateMultiChoiceDialogAdapter(
// This value was unselected // This value was unselected
notifyItemChanged(index, UncheckPayload) notifyItemChanged(index, UncheckPayload)
} }
current == QuadStateTextView.State.INDETERMINATE.ordinal && previous != QuadStateTextView.State.INDETERMINATE.ordinal -> {
// This value was set back to Indeterminate
notifyItemChanged(index, IndeterminatePayload)
}
} }
} }
} }
private var disabledIndices: IntArray = disabledItems ?: IntArray(0) private var disabledIndices: IntArray = disabledItems ?: IntArray(0)
internal fun itemActionClicked(index: Int) {
internal fun itemClicked(index: Int) {
val newSelection = this.currentSelection.toMutableList() val newSelection = this.currentSelection.toMutableList()
newSelection[index] = when (currentSelection[index]) { newSelection[index] = when (currentSelection[index]) {
QuadStateTextView.State.CHECKED.ordinal -> QuadStateTextView.State.INVERSED.ordinal QuadStateTextView.State.CHECKED.ordinal -> QuadStateTextView.State.INVERSED.ordinal
@ -56,6 +63,21 @@ internal class QuadStateMultiChoiceDialogAdapter(
listener(currentSelection) listener(currentSelection)
} }
internal fun itemDisplayClicked(index: Int) {
val newSelection = this.currentSelection.toMutableList()
newSelection[index] = when (currentSelection[index]) {
QuadStateTextView.State.UNCHECKED.ordinal -> QuadStateTextView.State.CHECKED.ordinal
QuadStateTextView.State.CHECKED.ordinal -> when (initialSelected[index]) {
QuadStateTextView.State.INDETERMINATE.ordinal -> QuadStateTextView.State.INDETERMINATE.ordinal
else -> QuadStateTextView.State.UNCHECKED.ordinal
}
// INDETERMINATE or UNCHECKED
else -> QuadStateTextView.State.UNCHECKED.ordinal
}
this.currentSelection = newSelection.toIntArray()
listener(currentSelection)
}
override fun onCreateViewHolder( override fun onCreateViewHolder(
parent: ViewGroup, parent: ViewGroup,
viewType: Int viewType: Int
@ -96,6 +118,10 @@ internal class QuadStateMultiChoiceDialogAdapter(
holder.controlView.state = QuadStateTextView.State.UNCHECKED holder.controlView.state = QuadStateTextView.State.UNCHECKED
return return
} }
IndeterminatePayload -> {
holder.controlView.state = QuadStateTextView.State.INDETERMINATE
return
}
} }
super.onBindViewHolder(holder, position, payloads) super.onBindViewHolder(holder, position, payloads)
} }

View file

@ -21,5 +21,8 @@ internal class QuadStateMultiChoiceViewHolder(
controlView.isEnabled = value controlView.isEnabled = value
} }
override fun onClick(view: View) = adapter.itemClicked(bindingAdapterPosition) override fun onClick(view: View) = when (adapter.isActionList) {
true -> adapter.itemActionClicked(bindingAdapterPosition)
false -> adapter.itemDisplayClicked(bindingAdapterPosition)
}
} }