mirror of
https://github.com/mihonapp/mihon.git
synced 2024-11-29 16:03:14 -05: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:
parent
c316e7faab
commit
ee711dc0fb
8 changed files with 121 additions and 31 deletions
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue