mirror of
				https://github.com/mihonapp/mihon.git
				synced 2025-10-31 06:17:57 +01:00 
			
		
		
		
	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:
		| @@ -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) { | ||||
|   | ||||
| @@ -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>()) | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -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() | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -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) | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -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) | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|   | ||||
| @@ -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 = { | ||||
|   | ||||
| @@ -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) | ||||
|     } | ||||
|   | ||||
| @@ -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) | ||||
|     } | ||||
| } | ||||
|   | ||||
		Reference in New Issue
	
	Block a user