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.widget.AutofitRecyclerView
import eu.kanade.tachiyomi.widget.EmptyView
import eu.kanade.tachiyomi.widget.materialdialogs.QuadStateTextView
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.drop
import kotlinx.coroutines.flow.launchIn
@ -626,8 +627,12 @@ open class BrowseSourceController(bundle: Bundle) :
// Choose a category
else -> {
val ids = presenter.getMangaCategoryIds(manga)
val preselected = ids.mapNotNull { id ->
categories.indexOfFirst { it.id == id }.takeIf { it != -1 }
val preselected = categories.map {
if (it.id in ids) {
QuadStateTextView.State.CHECKED.ordinal
} else {
QuadStateTextView.State.UNCHECKED.ordinal
}
}.toTypedArray()
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 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
presenter.changeMangaFavorite(manga)
presenter.updateMangaCategories(manga, categories)
presenter.updateMangaCategories(manga, addCategories)
val position = adapter?.currentItems?.indexOfFirst { it -> (it as SourceItem).manga.id == manga.id }
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.withFadeTransaction
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) :
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 categories = emptyList<Category>()
private var preselected = emptyArray<Int>()
private var selected = emptyArray<Int>().toIntArray()
constructor(
target: T,
@ -27,6 +30,7 @@ class ChangeMangaCategoriesDialog<T>(bundle: Bundle? = null) :
this.mangas = mangas
this.categories = categories
this.preselected = preselected
this.selected = preselected.toIntArray()
targetController = target
}
@ -36,15 +40,21 @@ class ChangeMangaCategoriesDialog<T>(bundle: Bundle? = null) :
.setNegativeButton(android.R.string.cancel, null)
.apply {
if (categories.isNotEmpty()) {
val selected = categories
.mapIndexed { i, _ -> preselected.contains(i) }
.toBooleanArray()
setMultiChoiceItems(categories.map { it.name }.toTypedArray(), selected) { _, which, checked ->
selected[which] = checked
setQuadStateMultiChoiceItems(
items = categories.map { it.name },
isActionList = false,
initialSelected = preselected.toIntArray()
) { selections ->
selected = selections
}
setPositiveButton(android.R.string.ok) { _, _ ->
val newCategories = categories.filterIndexed { i, _ -> selected[i] }
(targetController as? Listener)?.updateCategoriesForMangas(mangas, newCategories)
val add = selected
.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 {
setMessage(R.string.information_empty_category_dialog)
@ -62,6 +72,6 @@ class ChangeMangaCategoriesDialog<T>(bundle: Bundle? = null) :
}
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.toast
import eu.kanade.tachiyomi.widget.EmptyView
import eu.kanade.tachiyomi.widget.materialdialogs.QuadStateTextView
import kotlinx.coroutines.flow.drop
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
@ -558,11 +559,17 @@ class LibraryController(
val categories = presenter.categories.filter { it.id != 0 }
// Get indexes of the common categories to preselect.
val commonCategoriesIndexes = presenter.getCommonCategories(mangas)
.map { categories.indexOf(it) }
.toTypedArray()
ChangeMangaCategoriesDialog(this, mangas, categories, commonCategoriesIndexes)
val common = presenter.getCommonCategories(mangas)
// Get indexes of the mix categories to preselect.
val mix = presenter.getMixCategories(mangas)
var preselected = categories.map {
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)
}
@ -582,8 +589,8 @@ class LibraryController(
DeleteLibraryMangasDialog(this, selectedMangas.toList()).showDialog(router)
}
override fun updateCategoriesForMangas(mangas: List<Manga>, categories: List<Category>) {
presenter.moveMangasToCategories(categories, mangas)
override fun updateCategoriesForMangas(mangas: List<Manga>, addCategories: List<Category>, removeCategories: List<Category>) {
presenter.updateMangasToCategories(mangas, addCategories, removeCategories)
destroyActionModeIfNeeded()
}

View file

@ -442,6 +442,18 @@ class LibraryPresenter(
.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.
*
@ -533,4 +545,21 @@ class LibraryPresenter(
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.shrinkOnScroll
import eu.kanade.tachiyomi.util.view.snack
import eu.kanade.tachiyomi.widget.materialdialogs.QuadStateTextView
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import reactivecircus.flowbinding.recyclerview.scrollEvents
@ -578,8 +579,12 @@ class MangaController :
// Choose a category
else -> {
val ids = presenter.getMangaCategoryIds(manga)
val preselected = ids.mapNotNull { id ->
categories.indexOfFirst { it.id == id }.takeIf { it != -1 }
val preselected = categories.map {
if (it.id in ids) {
QuadStateTextView.State.CHECKED.ordinal
} else {
QuadStateTextView.State.UNCHECKED.ordinal
}
}.toTypedArray()
ChangeMangaCategoriesDialog(this, listOf(manga), categories, preselected)
@ -627,15 +632,18 @@ class MangaController :
val categories = presenter.getCategories()
val ids = presenter.getMangaCategoryIds(manga)
val preselected = ids.mapNotNull { id ->
categories.indexOfFirst { it.id == id }.takeIf { it != -1 }
val preselected = categories.map {
if (it.id in ids) {
QuadStateTextView.State.CHECKED.ordinal
} else {
QuadStateTextView.State.UNCHECKED.ordinal
}
}.toTypedArray()
ChangeMangaCategoriesDialog(this, listOf(manga), categories, preselected)
.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
if (!manga.favorite) {
@ -644,7 +652,7 @@ class MangaController :
activity?.invalidateOptionsMenu()
}
presenter.moveMangaToCategories(manga, categories)
presenter.moveMangaToCategories(manga, addCategories)
}
/**

View file

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

View file

@ -8,14 +8,18 @@ import eu.kanade.tachiyomi.databinding.DialogQuadstatemultichoiceItemBinding
private object CheckPayload
private object InverseCheckPayload
private object UncheckPayload
private object IndeterminatePayload
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 var items: List<CharSequence>,
disabledItems: IntArray?,
initialSelected: IntArray,
internal var listener: QuadStateMultiChoiceListener
private var initialSelected: IntArray,
internal var listener: QuadStateMultiChoiceListener,
val isActionList: Boolean = true
) : RecyclerView.Adapter<QuadStateMultiChoiceViewHolder>() {
private val states = QuadStateTextView.State.values()
@ -39,12 +43,15 @@ internal class QuadStateMultiChoiceDialogAdapter(
// This value was unselected
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)
internal fun itemClicked(index: Int) {
internal fun itemActionClicked(index: Int) {
val newSelection = this.currentSelection.toMutableList()
newSelection[index] = when (currentSelection[index]) {
QuadStateTextView.State.CHECKED.ordinal -> QuadStateTextView.State.INVERSED.ordinal
@ -56,6 +63,21 @@ internal class QuadStateMultiChoiceDialogAdapter(
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(
parent: ViewGroup,
viewType: Int
@ -96,6 +118,10 @@ internal class QuadStateMultiChoiceDialogAdapter(
holder.controlView.state = QuadStateTextView.State.UNCHECKED
return
}
IndeterminatePayload -> {
holder.controlView.state = QuadStateTextView.State.INDETERMINATE
return
}
}
super.onBindViewHolder(holder, position, payloads)
}

View file

@ -21,5 +21,8 @@ internal class QuadStateMultiChoiceViewHolder(
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)
}
}