diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceController.kt index 691c1af27..9ea5ee8e0 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceController.kt +++ b/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, categories: List) { + override fun updateCategoriesForMangas(mangas: List, addCategories: List, removeCategories: List) { 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) { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/ChangeMangaCategoriesDialog.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/ChangeMangaCategoriesDialog.kt index a443b8b2e..e393d4464 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/ChangeMangaCategoriesDialog.kt +++ b/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(bundle: Bundle? = null) : DialogController(bundle) where T : Controller, T : ChangeMangaCategoriesDialog.Listener { @@ -17,6 +19,7 @@ class ChangeMangaCategoriesDialog(bundle: Bundle? = null) : private var mangas = emptyList() private var categories = emptyList() private var preselected = emptyArray() + private var selected = emptyArray().toIntArray() constructor( target: T, @@ -27,6 +30,7 @@ class ChangeMangaCategoriesDialog(bundle: Bundle? = null) : this.mangas = mangas this.categories = categories this.preselected = preselected + this.selected = preselected.toIntArray() targetController = target } @@ -36,15 +40,21 @@ class ChangeMangaCategoriesDialog(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(bundle: Bundle? = null) : } interface Listener { - fun updateCategoriesForMangas(mangas: List, categories: List) + fun updateCategoriesForMangas(mangas: List, addCategories: List, removeCategories: List = emptyList()) } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryController.kt index 3d9476fdc..2ca72ac98 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryController.kt +++ b/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, categories: List) { - presenter.moveMangasToCategories(categories, mangas) + override fun updateCategoriesForMangas(mangas: List, addCategories: List, removeCategories: List) { + presenter.updateMangasToCategories(mangas, addCategories, removeCategories) destroyActionModeIfNeeded() } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryPresenter.kt index 9b959a0b1..7ead3b3c9 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryPresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryPresenter.kt @@ -442,6 +442,18 @@ class LibraryPresenter( .reduce { set1: Iterable, 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): Collection { + 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, addCategories: List, removeCategories: List) { + 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) + } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaController.kt index dc2b1c333..fe4ef1308 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaController.kt +++ b/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, categories: List) { + override fun updateCategoriesForMangas(mangas: List, addCategories: List, removeCategories: List) { val manga = mangas.firstOrNull() ?: return if (!manga.favorite) { @@ -644,7 +652,7 @@ class MangaController : activity?.invalidateOptionsMenu() } - presenter.moveMangaToCategories(manga, categories) + presenter.moveMangaToCategories(manga, addCategories) } /** diff --git a/app/src/main/java/eu/kanade/tachiyomi/widget/materialdialogs/MaterialAlertDialogBuilderExtensions.kt b/app/src/main/java/eu/kanade/tachiyomi/widget/materialdialogs/MaterialAlertDialogBuilderExtensions.kt index 7c98756d2..da8b93d91 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/widget/materialdialogs/MaterialAlertDialogBuilderExtensions.kt +++ b/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, initialSelected: IntArray, disabledIndices: IntArray? = null, @@ -50,6 +51,7 @@ fun MaterialAlertDialogBuilder.setQuadStateMultiChoiceItems( items = items, disabledItems = disabledIndices, initialSelected = initialSelected, + isActionList = isActionList, listener = selection ) val updateScrollIndicators = { diff --git a/app/src/main/java/eu/kanade/tachiyomi/widget/materialdialogs/QuadStateMultiChoiceDialogAdapter.kt b/app/src/main/java/eu/kanade/tachiyomi/widget/materialdialogs/QuadStateMultiChoiceDialogAdapter.kt index 9fcf29933..fb4bff7dd 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/widget/materialdialogs/QuadStateMultiChoiceDialogAdapter.kt +++ b/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, disabledItems: IntArray?, - initialSelected: IntArray, - internal var listener: QuadStateMultiChoiceListener + private var initialSelected: IntArray, + internal var listener: QuadStateMultiChoiceListener, + val isActionList: Boolean = true ) : RecyclerView.Adapter() { 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) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/widget/materialdialogs/QuadStateMultiChoiceViewHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/widget/materialdialogs/QuadStateMultiChoiceViewHolder.kt index fbdaee770..fc427b185 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/widget/materialdialogs/QuadStateMultiChoiceViewHolder.kt +++ b/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) + } }