mirror of
https://github.com/mihonapp/mihon.git
synced 2025-11-12 20:19:05 +01:00
Use Compose for Category screen (#7454)
* Use Compose for Category screen * Use correct string for CategoryRenameDialog title Co-authored-by: AntsyLich <59261191+AntsyLich@users.noreply.github.com> Co-authored-by: AntsyLich <59261191+AntsyLich@users.noreply.github.com>
This commit is contained in:
@@ -1,42 +0,0 @@
|
||||
package eu.kanade.tachiyomi.ui.category
|
||||
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
|
||||
/**
|
||||
* Custom adapter for categories.
|
||||
*
|
||||
* @param controller The containing controller.
|
||||
*/
|
||||
class CategoryAdapter(controller: CategoryController) :
|
||||
FlexibleAdapter<CategoryItem>(null, controller, true) {
|
||||
|
||||
/**
|
||||
* Listener called when an item of the list is released.
|
||||
*/
|
||||
val onItemReleaseListener: OnItemReleaseListener = controller
|
||||
|
||||
/**
|
||||
* Clears the active selections from the list and the model.
|
||||
*/
|
||||
override fun clearSelection() {
|
||||
super.clearSelection()
|
||||
(0 until itemCount).forEach { getItem(it)?.isSelected = false }
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggles the selection of the given position.
|
||||
*
|
||||
* @param position The position to toggle.
|
||||
*/
|
||||
override fun toggleSelection(position: Int) {
|
||||
super.toggleSelection(position)
|
||||
getItem(position)?.isSelected = isSelected(position)
|
||||
}
|
||||
|
||||
interface OnItemReleaseListener {
|
||||
/**
|
||||
* Called when an item of the list is released.
|
||||
*/
|
||||
fun onItemReleased(position: Int)
|
||||
}
|
||||
}
|
||||
@@ -1,357 +1,18 @@
|
||||
package eu.kanade.tachiyomi.ui.category
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.appcompat.view.ActionMode
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import dev.chrisbanes.insetter.applyInsetter
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
import eu.davidea.flexibleadapter.SelectableAdapter
|
||||
import eu.davidea.flexibleadapter.helpers.UndoHelper
|
||||
import eu.kanade.domain.category.model.Category
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.databinding.CategoriesControllerBinding
|
||||
import eu.kanade.tachiyomi.ui.base.controller.FabController
|
||||
import eu.kanade.tachiyomi.ui.base.controller.NucleusController
|
||||
import eu.kanade.tachiyomi.ui.main.MainActivity
|
||||
import eu.kanade.tachiyomi.util.system.toast
|
||||
import eu.kanade.tachiyomi.util.view.shrinkOnScroll
|
||||
import kotlinx.coroutines.launch
|
||||
import androidx.compose.runtime.Composable
|
||||
import eu.kanade.presentation.category.CategoryScreen
|
||||
import eu.kanade.tachiyomi.ui.base.controller.FullComposeController
|
||||
|
||||
/**
|
||||
* Controller to manage the categories for the users' library.
|
||||
*/
|
||||
class CategoryController :
|
||||
NucleusController<CategoriesControllerBinding, CategoryPresenter>(),
|
||||
FabController,
|
||||
ActionMode.Callback,
|
||||
FlexibleAdapter.OnItemClickListener,
|
||||
FlexibleAdapter.OnItemLongClickListener,
|
||||
CategoryAdapter.OnItemReleaseListener,
|
||||
CategoryCreateDialog.Listener,
|
||||
CategoryRenameDialog.Listener,
|
||||
UndoHelper.OnActionListener {
|
||||
class CategoryController : FullComposeController<CategoryPresenter>() {
|
||||
|
||||
/**
|
||||
* Object used to show ActionMode toolbar.
|
||||
*/
|
||||
private var actionMode: ActionMode? = null
|
||||
|
||||
/**
|
||||
* Adapter containing category items.
|
||||
*/
|
||||
private var adapter: CategoryAdapter? = null
|
||||
|
||||
private var actionFab: ExtendedFloatingActionButton? = null
|
||||
private var actionFabScrollListener: RecyclerView.OnScrollListener? = null
|
||||
|
||||
/**
|
||||
* Undo helper used for restoring a deleted category.
|
||||
*/
|
||||
private var undoHelper: UndoHelper? = null
|
||||
|
||||
/**
|
||||
* Creates the presenter for this controller. Not to be manually called.
|
||||
*/
|
||||
override fun createPresenter() = CategoryPresenter()
|
||||
|
||||
/**
|
||||
* Returns the toolbar title to show when this controller is attached.
|
||||
*/
|
||||
override fun getTitle(): String? {
|
||||
return resources?.getString(R.string.action_edit_categories)
|
||||
}
|
||||
|
||||
override fun createBinding(inflater: LayoutInflater) = CategoriesControllerBinding.inflate(inflater)
|
||||
|
||||
/**
|
||||
* Called after view inflation. Used to initialize the view.
|
||||
*
|
||||
* @param view The view of this controller.
|
||||
*/
|
||||
override fun onViewCreated(view: View) {
|
||||
super.onViewCreated(view)
|
||||
|
||||
binding.recycler.applyInsetter {
|
||||
type(navigationBars = true) {
|
||||
padding()
|
||||
}
|
||||
}
|
||||
|
||||
adapter = CategoryAdapter(this@CategoryController)
|
||||
binding.recycler.layoutManager = LinearLayoutManager(view.context)
|
||||
binding.recycler.setHasFixedSize(true)
|
||||
binding.recycler.adapter = adapter
|
||||
adapter?.isHandleDragEnabled = true
|
||||
adapter?.isPermanentDelete = false
|
||||
|
||||
actionFabScrollListener = actionFab?.shrinkOnScroll(binding.recycler)
|
||||
|
||||
viewScope.launch {
|
||||
presenter.categories.collect {
|
||||
setCategories(it.map(::CategoryItem))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun configureFab(fab: ExtendedFloatingActionButton) {
|
||||
actionFab = fab
|
||||
fab.setText(R.string.action_add)
|
||||
fab.setIconResource(R.drawable.ic_add_24dp)
|
||||
fab.setOnClickListener {
|
||||
CategoryCreateDialog(this@CategoryController).showDialog(router, null)
|
||||
}
|
||||
}
|
||||
|
||||
override fun cleanupFab(fab: ExtendedFloatingActionButton) {
|
||||
fab.setOnClickListener(null)
|
||||
actionFabScrollListener?.let { binding.recycler.removeOnScrollListener(it) }
|
||||
actionFab = null
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the view is being destroyed. Used to release references and remove callbacks.
|
||||
*
|
||||
* @param view The view of this controller.
|
||||
*/
|
||||
override fun onDestroyView(view: View) {
|
||||
// Manually call callback to delete categories if required
|
||||
undoHelper?.onDeleteConfirmed(Snackbar.Callback.DISMISS_EVENT_MANUAL)
|
||||
undoHelper = null
|
||||
actionMode = null
|
||||
adapter = null
|
||||
super.onDestroyView(view)
|
||||
}
|
||||
|
||||
/**
|
||||
* Called from the presenter when the categories are updated.
|
||||
*
|
||||
* @param categories The new list of categories to display.
|
||||
*/
|
||||
fun setCategories(categories: List<CategoryItem>) {
|
||||
actionMode?.finish()
|
||||
adapter?.updateDataSet(categories)
|
||||
if (categories.isNotEmpty()) {
|
||||
binding.emptyView.hide()
|
||||
val selected = categories.filter { it.isSelected }
|
||||
if (selected.isNotEmpty()) {
|
||||
selected.forEach { onItemLongClick(categories.indexOf(it)) }
|
||||
}
|
||||
} else {
|
||||
binding.emptyView.show(R.string.information_empty_category)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when action mode is first created. The menu supplied will be used to generate action
|
||||
* buttons for the action mode.
|
||||
*
|
||||
* @param mode ActionMode being created.
|
||||
* @param menu Menu used to populate action buttons.
|
||||
* @return true if the action mode should be created, false if entering this mode should be
|
||||
* aborted.
|
||||
*/
|
||||
override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean {
|
||||
// Inflate menu.
|
||||
mode.menuInflater.inflate(R.menu.category_selection, menu)
|
||||
// Enable adapter multi selection.
|
||||
adapter?.mode = SelectableAdapter.Mode.MULTI
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Called to refresh an action mode's action menu whenever it is invalidated.
|
||||
*
|
||||
* @param mode ActionMode being prepared.
|
||||
* @param menu Menu used to populate action buttons.
|
||||
* @return true if the menu or action mode was updated, false otherwise.
|
||||
*/
|
||||
override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean {
|
||||
val adapter = adapter ?: return false
|
||||
val count = adapter.selectedItemCount
|
||||
mode.title = count.toString()
|
||||
|
||||
// Show edit button only when one item is selected
|
||||
val editItem = mode.menu.findItem(R.id.action_edit)
|
||||
editItem.isVisible = count == 1
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Called to report a user click on an action button.
|
||||
*
|
||||
* @param mode The current ActionMode.
|
||||
* @param item The item that was clicked.
|
||||
* @return true if this callback handled the event, false if the standard MenuItem invocation
|
||||
* should continue.
|
||||
*/
|
||||
override fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean {
|
||||
val adapter = adapter ?: return false
|
||||
|
||||
when (item.itemId) {
|
||||
R.id.action_delete -> {
|
||||
undoHelper = UndoHelper(adapter, this)
|
||||
undoHelper?.start(
|
||||
adapter.selectedPositions,
|
||||
(activity as? MainActivity)?.binding?.rootCoordinator!!,
|
||||
R.string.snack_categories_deleted,
|
||||
R.string.action_undo,
|
||||
4000,
|
||||
)
|
||||
|
||||
mode.finish()
|
||||
}
|
||||
R.id.action_edit -> {
|
||||
// Edit selected category
|
||||
if (adapter.selectedItemCount == 1) {
|
||||
val position = adapter.selectedPositions.first()
|
||||
val category = adapter.getItem(position)?.category
|
||||
if (category != null) {
|
||||
editCategory(category)
|
||||
}
|
||||
}
|
||||
}
|
||||
else -> return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when an action mode is about to be exited and destroyed.
|
||||
*
|
||||
* @param mode The current ActionMode being destroyed.
|
||||
*/
|
||||
override fun onDestroyActionMode(mode: ActionMode) {
|
||||
// Reset adapter to single selection
|
||||
adapter?.mode = SelectableAdapter.Mode.IDLE
|
||||
adapter?.clearSelection()
|
||||
actionMode = null
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when an item in the list is clicked.
|
||||
*
|
||||
* @param position The position of the clicked item.
|
||||
* @return true if this click should enable selection mode.
|
||||
*/
|
||||
override fun onItemClick(view: View, position: Int): Boolean {
|
||||
// Check if action mode is initialized and selected item exist.
|
||||
return if (actionMode != null && position != RecyclerView.NO_POSITION) {
|
||||
toggleSelection(position)
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when an item in the list is long clicked.
|
||||
*
|
||||
* @param position The position of the clicked item.
|
||||
*/
|
||||
override fun onItemLongClick(position: Int) {
|
||||
val activity = activity as? AppCompatActivity ?: return
|
||||
|
||||
// Check if action mode is initialized.
|
||||
if (actionMode == null) {
|
||||
// Initialize action mode
|
||||
actionMode = activity.startSupportActionMode(this)
|
||||
}
|
||||
|
||||
// Set item as selected
|
||||
toggleSelection(position)
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle the selection state of an item.
|
||||
* If the item was the last one in the selection and is unselected, the ActionMode is finished.
|
||||
*
|
||||
* @param position The position of the item to toggle.
|
||||
*/
|
||||
private fun toggleSelection(position: Int) {
|
||||
val adapter = adapter ?: return
|
||||
|
||||
// Mark the position selected
|
||||
adapter.toggleSelection(position)
|
||||
|
||||
if (adapter.selectedItemCount == 0) {
|
||||
actionMode?.finish()
|
||||
} else {
|
||||
actionMode?.invalidate()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when an item is released from a drag.
|
||||
*
|
||||
* @param position The position of the released item.
|
||||
*/
|
||||
override fun onItemReleased(position: Int) {
|
||||
val adapter = adapter ?: return
|
||||
val categories = (0 until adapter.itemCount).mapNotNull { adapter.getItem(it)?.category }
|
||||
presenter.reorderCategories(categories)
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the undo action is clicked in the snackbar.
|
||||
*
|
||||
* @param action The action performed.
|
||||
*/
|
||||
override fun onActionCanceled(action: Int, positions: MutableList<Int>?) {
|
||||
adapter?.restoreDeletedItems()
|
||||
undoHelper = null
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the time to restore the items expires.
|
||||
*
|
||||
* @param action The action performed.
|
||||
* @param event The event that triggered the action
|
||||
*/
|
||||
override fun onActionConfirmed(action: Int, event: Int) {
|
||||
val adapter = adapter ?: return
|
||||
presenter.deleteCategories(adapter.deletedItems.map { it.category })
|
||||
undoHelper = null
|
||||
}
|
||||
|
||||
/**
|
||||
* Show a dialog to let the user change the category name.
|
||||
*
|
||||
* @param category The category to be edited.
|
||||
*/
|
||||
private fun editCategory(category: Category) {
|
||||
CategoryRenameDialog(this, category).showDialog(router)
|
||||
}
|
||||
|
||||
/**
|
||||
* Renames the given category with the given name.
|
||||
*
|
||||
* @param category The category to rename.
|
||||
* @param name The new name of the category.
|
||||
*/
|
||||
override fun renameCategory(category: Category, name: String) {
|
||||
presenter.renameCategory(category, name)
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new category with the given name.
|
||||
*
|
||||
* @param name The name of the new category.
|
||||
*/
|
||||
override fun createCategory(name: String) {
|
||||
presenter.createCategory(name)
|
||||
}
|
||||
|
||||
/**
|
||||
* Called from the presenter when a category with the given name already exists.
|
||||
*/
|
||||
fun onCategoryExistsError() {
|
||||
activity?.toast(R.string.error_category_exists)
|
||||
@Composable
|
||||
override fun ComposeContent() {
|
||||
CategoryScreen(
|
||||
presenter = presenter,
|
||||
navigateUp = router::popCurrentController,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
package eu.kanade.tachiyomi.ui.category
|
||||
|
||||
import android.app.Dialog
|
||||
import android.os.Bundle
|
||||
import com.bluelinelabs.conductor.Controller
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.ui.base.controller.DialogController
|
||||
import eu.kanade.tachiyomi.widget.materialdialogs.setTextInput
|
||||
|
||||
/**
|
||||
* Dialog to create a new category for the library.
|
||||
*/
|
||||
class CategoryCreateDialog<T>(bundle: Bundle? = null) : DialogController(bundle)
|
||||
where T : Controller, T : CategoryCreateDialog.Listener {
|
||||
|
||||
/**
|
||||
* Name of the new category. Value updated with each input from the user.
|
||||
*/
|
||||
private var currentName = ""
|
||||
|
||||
constructor(target: T) : this() {
|
||||
targetController = target
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when creating the dialog for this controller.
|
||||
*
|
||||
* @param savedViewState The saved state of this dialog.
|
||||
* @return a new dialog instance.
|
||||
*/
|
||||
override fun onCreateDialog(savedViewState: Bundle?): Dialog {
|
||||
return MaterialAlertDialogBuilder(activity!!)
|
||||
.setTitle(R.string.action_add_category)
|
||||
.setTextInput(prefill = currentName) {
|
||||
currentName = it
|
||||
}
|
||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
(targetController as? Listener)?.createCategory(currentName)
|
||||
}
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.create()
|
||||
}
|
||||
|
||||
interface Listener {
|
||||
fun createCategory(name: String)
|
||||
}
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
package eu.kanade.tachiyomi.ui.category
|
||||
|
||||
import android.view.View
|
||||
import androidx.recyclerview.widget.ItemTouchHelper
|
||||
import eu.davidea.viewholders.FlexibleViewHolder
|
||||
import eu.kanade.domain.category.model.Category
|
||||
import eu.kanade.tachiyomi.databinding.CategoriesItemBinding
|
||||
|
||||
/**
|
||||
* Holder used to display category items.
|
||||
*
|
||||
* @param view The view used by category items.
|
||||
* @param adapter The adapter containing this holder.
|
||||
*/
|
||||
class CategoryHolder(view: View, val adapter: CategoryAdapter) : FlexibleViewHolder(view, adapter) {
|
||||
|
||||
private val binding = CategoriesItemBinding.bind(view)
|
||||
|
||||
init {
|
||||
setDragHandleView(binding.reorder)
|
||||
}
|
||||
|
||||
/**
|
||||
* Binds this holder with the given category.
|
||||
*
|
||||
* @param category The category to bind.
|
||||
*/
|
||||
fun bind(category: Category) {
|
||||
binding.title.text = category.name
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when an item is released.
|
||||
*
|
||||
* @param position The position of the released item.
|
||||
*/
|
||||
override fun onItemReleased(position: Int) {
|
||||
super.onItemReleased(position)
|
||||
adapter.onItemReleaseListener.onItemReleased(position)
|
||||
binding.container.isDragged = false
|
||||
}
|
||||
|
||||
override fun onActionStateChanged(position: Int, actionState: Int) {
|
||||
super.onActionStateChanged(position, actionState)
|
||||
if (actionState == ItemTouchHelper.ACTION_STATE_DRAG) {
|
||||
binding.container.isDragged = true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,73 +0,0 @@
|
||||
package eu.kanade.tachiyomi.ui.category
|
||||
|
||||
import android.view.View
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
|
||||
import eu.davidea.flexibleadapter.items.IFlexible
|
||||
import eu.kanade.domain.category.model.Category
|
||||
import eu.kanade.tachiyomi.R
|
||||
|
||||
/**
|
||||
* Category item for a recycler view.
|
||||
*/
|
||||
class CategoryItem(val category: Category) : AbstractFlexibleItem<CategoryHolder>() {
|
||||
|
||||
/**
|
||||
* Whether this item is currently selected.
|
||||
*/
|
||||
var isSelected = false
|
||||
|
||||
/**
|
||||
* Returns the layout resource for this item.
|
||||
*/
|
||||
override fun getLayoutRes(): Int {
|
||||
return R.layout.categories_item
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new view holder for this item.
|
||||
*
|
||||
* @param view The view of this item.
|
||||
* @param adapter The adapter of this item.
|
||||
*/
|
||||
override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>): CategoryHolder {
|
||||
return CategoryHolder(view, adapter as CategoryAdapter)
|
||||
}
|
||||
|
||||
/**
|
||||
* Binds the given view holder with this item.
|
||||
*
|
||||
* @param adapter The adapter of this item.
|
||||
* @param holder The holder to bind.
|
||||
* @param position The position of this item in the adapter.
|
||||
* @param payloads List of partial changes.
|
||||
*/
|
||||
override fun bindViewHolder(
|
||||
adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>,
|
||||
holder: CategoryHolder,
|
||||
position: Int,
|
||||
payloads: List<Any?>?,
|
||||
) {
|
||||
holder.bind(category)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if this item is draggable.
|
||||
*/
|
||||
override fun isDraggable(): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (other is CategoryItem) {
|
||||
return category.id == other.category.id
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
return category.id.hashCode()
|
||||
}
|
||||
}
|
||||
@@ -1,130 +1,91 @@
|
||||
package eu.kanade.tachiyomi.ui.category
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import eu.kanade.domain.category.interactor.CreateCategoryWithName
|
||||
import eu.kanade.domain.category.interactor.DeleteCategory
|
||||
import eu.kanade.domain.category.interactor.GetCategories
|
||||
import eu.kanade.domain.category.interactor.InsertCategory
|
||||
import eu.kanade.domain.category.interactor.UpdateCategory
|
||||
import eu.kanade.domain.category.interactor.RenameCategory
|
||||
import eu.kanade.domain.category.interactor.ReorderCategory
|
||||
import eu.kanade.domain.category.model.Category
|
||||
import eu.kanade.domain.category.model.CategoryUpdate
|
||||
import eu.kanade.domain.category.repository.DuplicateNameException
|
||||
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
|
||||
import eu.kanade.tachiyomi.util.lang.launchIO
|
||||
import eu.kanade.tachiyomi.util.lang.launchUI
|
||||
import eu.kanade.tachiyomi.util.system.logcat
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import logcat.LogPriority
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.flow.consumeAsFlow
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
|
||||
/**
|
||||
* Presenter of [CategoryController]. Used to manage the categories of the library.
|
||||
*/
|
||||
class CategoryPresenter(
|
||||
private val getCategories: GetCategories = Injekt.get(),
|
||||
private val insertCategory: InsertCategory = Injekt.get(),
|
||||
private val updateCategory: UpdateCategory = Injekt.get(),
|
||||
private val createCategoryWithName: CreateCategoryWithName = Injekt.get(),
|
||||
private val renameCategory: RenameCategory = Injekt.get(),
|
||||
private val reorderCategory: ReorderCategory = Injekt.get(),
|
||||
private val deleteCategory: DeleteCategory = Injekt.get(),
|
||||
) : BasePresenter<CategoryController>() {
|
||||
|
||||
private val _categories: MutableStateFlow<List<Category>> = MutableStateFlow(listOf())
|
||||
val categories = _categories.asStateFlow()
|
||||
var dialog: Dialog? by mutableStateOf(null)
|
||||
|
||||
/**
|
||||
* Called when the presenter is created.
|
||||
*
|
||||
* @param savedState The saved state of this presenter.
|
||||
*/
|
||||
override fun onCreate(savedState: Bundle?) {
|
||||
super.onCreate(savedState)
|
||||
val categories = getCategories.subscribe()
|
||||
|
||||
presenterScope.launchIO {
|
||||
getCategories.subscribe()
|
||||
.collectLatest { list ->
|
||||
_categories.value = list
|
||||
}
|
||||
}
|
||||
}
|
||||
private val _events: Channel<Event> = Channel(Int.MAX_VALUE)
|
||||
val events = _events.consumeAsFlow()
|
||||
|
||||
/**
|
||||
* Creates and adds a new category to the database.
|
||||
*
|
||||
* @param name The name of the category to create.
|
||||
*/
|
||||
fun createCategory(name: String) {
|
||||
presenterScope.launchIO {
|
||||
val result = insertCategory.await(
|
||||
name = name,
|
||||
order = categories.value.map { it.order + 1L }.maxOrNull() ?: 0L,
|
||||
)
|
||||
when (result) {
|
||||
is InsertCategory.Result.Success -> {}
|
||||
is InsertCategory.Result.Error -> {
|
||||
logcat(LogPriority.ERROR, result.error)
|
||||
if (result.error is DuplicateNameException) {
|
||||
launchUI { view?.onCategoryExistsError() }
|
||||
}
|
||||
}
|
||||
when (createCategoryWithName.await(name)) {
|
||||
is CreateCategoryWithName.Result.NameAlreadyExistsError -> _events.send(Event.CategoryWithNameAlreadyExists)
|
||||
is CreateCategoryWithName.Result.InternalError -> _events.send(Event.InternalError)
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the given categories from the database.
|
||||
*
|
||||
* @param categories The list of categories to delete.
|
||||
*/
|
||||
fun deleteCategories(categories: List<Category>) {
|
||||
fun deleteCategory(category: Category) {
|
||||
presenterScope.launchIO {
|
||||
categories.forEach { category ->
|
||||
deleteCategory.await(category.id)
|
||||
when (deleteCategory.await(category.id)) {
|
||||
is DeleteCategory.Result.InternalError -> _events.send(Event.InternalError)
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reorders the given categories in the database.
|
||||
*
|
||||
* @param categories The list of categories to reorder.
|
||||
*/
|
||||
fun reorderCategories(categories: List<Category>) {
|
||||
fun moveUp(category: Category) {
|
||||
presenterScope.launchIO {
|
||||
categories.forEachIndexed { order, category ->
|
||||
updateCategory.await(
|
||||
payload = CategoryUpdate(
|
||||
id = category.id,
|
||||
order = order.toLong(),
|
||||
),
|
||||
)
|
||||
when (reorderCategory.await(category, category.order - 1)) {
|
||||
is ReorderCategory.Result.InternalError -> _events.send(Event.InternalError)
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun moveDown(category: Category) {
|
||||
presenterScope.launchIO {
|
||||
when (reorderCategory.await(category, category.order + 1)) {
|
||||
is ReorderCategory.Result.InternalError -> _events.send(Event.InternalError)
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Renames a category.
|
||||
*
|
||||
* @param category The category to rename.
|
||||
* @param name The new name of the category.
|
||||
*/
|
||||
fun renameCategory(category: Category, name: String) {
|
||||
presenterScope.launchIO {
|
||||
val result = updateCategory.await(
|
||||
payload = CategoryUpdate(
|
||||
id = category.id,
|
||||
name = name,
|
||||
),
|
||||
)
|
||||
when (result) {
|
||||
is UpdateCategory.Result.Success -> {}
|
||||
is UpdateCategory.Result.Error -> {
|
||||
logcat(LogPriority.ERROR, result.error)
|
||||
if (result.error is DuplicateNameException) {
|
||||
launchUI { view?.onCategoryExistsError() }
|
||||
}
|
||||
}
|
||||
when (renameCategory.await(category, name)) {
|
||||
RenameCategory.Result.NameAlreadyExistsError -> _events.send(Event.CategoryWithNameAlreadyExists)
|
||||
is RenameCategory.Result.InternalError -> _events.send(Event.InternalError)
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sealed class Dialog {
|
||||
object Create : Dialog()
|
||||
data class Rename(val category: Category) : Dialog()
|
||||
data class Delete(val category: Category) : Dialog()
|
||||
}
|
||||
|
||||
sealed class Event {
|
||||
object CategoryWithNameAlreadyExists : Event()
|
||||
object InternalError : Event()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,83 +0,0 @@
|
||||
package eu.kanade.tachiyomi.ui.category
|
||||
|
||||
import android.app.Dialog
|
||||
import android.os.Bundle
|
||||
import com.bluelinelabs.conductor.Controller
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import eu.kanade.domain.category.model.Category
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.ui.base.controller.DialogController
|
||||
import eu.kanade.tachiyomi.widget.materialdialogs.setTextInput
|
||||
|
||||
/**
|
||||
* Dialog to rename an existing category of the library.
|
||||
*/
|
||||
class CategoryRenameDialog<T>(bundle: Bundle? = null) : DialogController(bundle)
|
||||
where T : Controller, T : CategoryRenameDialog.Listener {
|
||||
|
||||
private var category: Category? = null
|
||||
|
||||
/**
|
||||
* Name of the new category. Value updated with each input from the user.
|
||||
*/
|
||||
private var currentName = ""
|
||||
|
||||
constructor(target: T, category: Category) : this() {
|
||||
targetController = target
|
||||
this.category = category
|
||||
currentName = category.name
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when creating the dialog for this controller.
|
||||
*
|
||||
* @param savedViewState The saved state of this dialog.
|
||||
* @return a new dialog instance.
|
||||
*/
|
||||
override fun onCreateDialog(savedViewState: Bundle?): Dialog {
|
||||
return MaterialAlertDialogBuilder(activity!!)
|
||||
.setTitle(R.string.action_rename_category)
|
||||
.setTextInput(prefill = currentName) {
|
||||
currentName = it
|
||||
}
|
||||
.setPositiveButton(android.R.string.ok) { _, _ -> onPositive() }
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.create()
|
||||
}
|
||||
|
||||
/**
|
||||
* Called to save this Controller's state in the event that its host Activity is destroyed.
|
||||
*
|
||||
* @param outState The Bundle into which data should be saved
|
||||
*/
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
outState.putSerializable(CATEGORY_KEY, category)
|
||||
super.onSaveInstanceState(outState)
|
||||
}
|
||||
|
||||
/**
|
||||
* Restores data that was saved in the [onSaveInstanceState] method.
|
||||
*
|
||||
* @param savedInstanceState The bundle that has data to be restored
|
||||
*/
|
||||
override fun onRestoreInstanceState(savedInstanceState: Bundle) {
|
||||
super.onRestoreInstanceState(savedInstanceState)
|
||||
category = savedInstanceState.getSerializable(CATEGORY_KEY) as? Category
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the positive button of the dialog is clicked.
|
||||
*/
|
||||
private fun onPositive() {
|
||||
val target = targetController as? Listener ?: return
|
||||
val category = category ?: return
|
||||
|
||||
target.renameCategory(category, currentName)
|
||||
}
|
||||
|
||||
interface Listener {
|
||||
fun renameCategory(category: Category, name: String)
|
||||
}
|
||||
}
|
||||
|
||||
private const val CATEGORY_KEY = "CategoryRenameDialog.category"
|
||||
Reference in New Issue
Block a user