瀏覽代碼

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 <[email protected]>
Quang Kieu 3 年之前
父節點
當前提交
ee711dc0fb

+ 9 - 4
app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceController.kt

@@ -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) {

+ 18 - 8
app/src/main/java/eu/kanade/tachiyomi/ui/library/ChangeMangaCategoriesDialog.kt

@@ -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>())
     }
 }

+ 14 - 7
app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryController.kt

@@ -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()
     }
 

+ 29 - 0
app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryPresenter.kt

@@ -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)
+    }
 }

+ 15 - 7
app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaController.kt

@@ -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)
     }
 
     /**

+ 2 - 0
app/src/main/java/eu/kanade/tachiyomi/widget/materialdialogs/MaterialAlertDialogBuilderExtensions.kt

@@ -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 = {

+ 30 - 4
app/src/main/java/eu/kanade/tachiyomi/widget/materialdialogs/QuadStateMultiChoiceDialogAdapter.kt

@@ -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)
     }

+ 4 - 1
app/src/main/java/eu/kanade/tachiyomi/widget/materialdialogs/QuadStateMultiChoiceViewHolder.kt

@@ -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)
+    }
 }