mirror of
https://github.com/aniyomiorg/aniyomi.git
synced 2024-11-25 06:11:04 +03:00
add anime categories
This commit is contained in:
parent
f447127413
commit
9f1f025def
15 changed files with 798 additions and 19 deletions
|
@ -224,8 +224,12 @@ object PreferenceKeys {
|
||||||
|
|
||||||
const val categoryTabs = "display_category_tabs"
|
const val categoryTabs = "display_category_tabs"
|
||||||
|
|
||||||
|
const val animeCategoryTabs = "display_anime_category_tabs"
|
||||||
|
|
||||||
const val categoryNumberOfItems = "display_number_of_items"
|
const val categoryNumberOfItems = "display_number_of_items"
|
||||||
|
|
||||||
|
const val animeCategoryNumberOfItems = "display_number_of_items_anime"
|
||||||
|
|
||||||
const val alwaysShowChapterTransition = "always_show_chapter_transition"
|
const val alwaysShowChapterTransition = "always_show_chapter_transition"
|
||||||
|
|
||||||
const val searchPinnedSourcesOnly = "search_pinned_sources_only"
|
const val searchPinnedSourcesOnly = "search_pinned_sources_only"
|
||||||
|
|
|
@ -272,8 +272,12 @@ class PreferencesHelper(val context: Context) {
|
||||||
|
|
||||||
fun categoryTabs() = flowPrefs.getBoolean(Keys.categoryTabs, true)
|
fun categoryTabs() = flowPrefs.getBoolean(Keys.categoryTabs, true)
|
||||||
|
|
||||||
|
fun animeCategoryTabs() = flowPrefs.getBoolean(Keys.animeCategoryTabs, true)
|
||||||
|
|
||||||
fun categoryNumberOfItems() = flowPrefs.getBoolean(Keys.categoryNumberOfItems, false)
|
fun categoryNumberOfItems() = flowPrefs.getBoolean(Keys.categoryNumberOfItems, false)
|
||||||
|
|
||||||
|
fun animeCategoryNumberOfItems() = flowPrefs.getBoolean(Keys.animeCategoryNumberOfItems, false)
|
||||||
|
|
||||||
fun filterDownloaded() = flowPrefs.getInt(Keys.filterDownloaded, ExtendedNavigationView.Item.TriStateGroup.State.IGNORE.value)
|
fun filterDownloaded() = flowPrefs.getInt(Keys.filterDownloaded, ExtendedNavigationView.Item.TriStateGroup.State.IGNORE.value)
|
||||||
|
|
||||||
fun filterUnread() = flowPrefs.getInt(Keys.filterUnread, ExtendedNavigationView.Item.TriStateGroup.State.IGNORE.value)
|
fun filterUnread() = flowPrefs.getInt(Keys.filterUnread, ExtendedNavigationView.Item.TriStateGroup.State.IGNORE.value)
|
||||||
|
|
|
@ -0,0 +1,42 @@
|
||||||
|
package eu.kanade.tachiyomi.ui.animecategory
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,350 @@
|
||||||
|
package eu.kanade.tachiyomi.ui.animecategory
|
||||||
|
|
||||||
|
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.tachiyomi.R
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Category
|
||||||
|
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
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
3000
|
||||||
|
)
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,50 @@
|
||||||
|
package eu.kanade.tachiyomi.ui.animecategory
|
||||||
|
|
||||||
|
import android.app.Dialog
|
||||||
|
import android.os.Bundle
|
||||||
|
import com.afollestad.materialdialogs.MaterialDialog
|
||||||
|
import com.afollestad.materialdialogs.input.input
|
||||||
|
import com.bluelinelabs.conductor.Controller
|
||||||
|
import eu.kanade.tachiyomi.R
|
||||||
|
import eu.kanade.tachiyomi.ui.base.controller.DialogController
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 MaterialDialog(activity!!)
|
||||||
|
.title(R.string.action_add_category)
|
||||||
|
.negativeButton(android.R.string.cancel)
|
||||||
|
.input(
|
||||||
|
hint = resources?.getString(R.string.name),
|
||||||
|
prefill = currentName
|
||||||
|
) { _, input ->
|
||||||
|
currentName = input.toString()
|
||||||
|
}
|
||||||
|
.positiveButton(android.R.string.ok) {
|
||||||
|
(targetController as? Listener)?.createCategory(currentName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Listener {
|
||||||
|
fun createCategory(name: String)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,40 @@
|
||||||
|
package eu.kanade.tachiyomi.ui.animecategory
|
||||||
|
|
||||||
|
import android.view.View
|
||||||
|
import eu.davidea.viewholders.FlexibleViewHolder
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,73 @@
|
||||||
|
package eu.kanade.tachiyomi.ui.animecategory
|
||||||
|
|
||||||
|
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.tachiyomi.R
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Category
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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!!
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,106 @@
|
||||||
|
package eu.kanade.tachiyomi.ui.animecategory
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import eu.kanade.tachiyomi.data.database.AnimeDatabaseHelper
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Category
|
||||||
|
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
|
||||||
|
import rx.Observable
|
||||||
|
import rx.android.schedulers.AndroidSchedulers
|
||||||
|
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 db: AnimeDatabaseHelper = Injekt.get()
|
||||||
|
) : BasePresenter<CategoryController>() {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List containing categories.
|
||||||
|
*/
|
||||||
|
private var categories: List<Category> = emptyList()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when the presenter is created.
|
||||||
|
*
|
||||||
|
* @param savedState The saved state of this presenter.
|
||||||
|
*/
|
||||||
|
override fun onCreate(savedState: Bundle?) {
|
||||||
|
super.onCreate(savedState)
|
||||||
|
|
||||||
|
db.getCategories().asRxObservable()
|
||||||
|
.doOnNext { categories = it }
|
||||||
|
.map { it.map(::CategoryItem) }
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.subscribeLatestCache(CategoryController::setCategories)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates and adds a new category to the database.
|
||||||
|
*
|
||||||
|
* @param name The name of the category to create.
|
||||||
|
*/
|
||||||
|
fun createCategory(name: String) {
|
||||||
|
// Do not allow duplicate categories.
|
||||||
|
if (categoryExists(name)) {
|
||||||
|
Observable.just(Unit).subscribeFirst({ view, _ -> view.onCategoryExistsError() })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create category.
|
||||||
|
val cat = Category.create(name)
|
||||||
|
|
||||||
|
// Set the new item in the last position.
|
||||||
|
cat.order = categories.map { it.order + 1 }.maxOrNull() ?: 0
|
||||||
|
|
||||||
|
// Insert into database.
|
||||||
|
db.insertCategory(cat).asRxObservable().subscribe()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes the given categories from the database.
|
||||||
|
*
|
||||||
|
* @param categories The list of categories to delete.
|
||||||
|
*/
|
||||||
|
fun deleteCategories(categories: List<Category>) {
|
||||||
|
db.deleteCategories(categories).asRxObservable().subscribe()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reorders the given categories in the database.
|
||||||
|
*
|
||||||
|
* @param categories The list of categories to reorder.
|
||||||
|
*/
|
||||||
|
fun reorderCategories(categories: List<Category>) {
|
||||||
|
categories.forEachIndexed { i, category ->
|
||||||
|
category.order = i
|
||||||
|
}
|
||||||
|
|
||||||
|
db.insertCategories(categories).asRxObservable().subscribe()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renames a category.
|
||||||
|
*
|
||||||
|
* @param category The category to rename.
|
||||||
|
* @param name The new name of the category.
|
||||||
|
*/
|
||||||
|
fun renameCategory(category: Category, name: String) {
|
||||||
|
// Do not allow duplicate categories.
|
||||||
|
if (categoryExists(name)) {
|
||||||
|
Observable.just(Unit).subscribeFirst({ view, _ -> view.onCategoryExistsError() })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
category.name = name
|
||||||
|
db.insertCategory(category).asRxObservable().subscribe()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if a category with the given name already exists.
|
||||||
|
*/
|
||||||
|
private fun categoryExists(name: String): Boolean {
|
||||||
|
return categories.any { it.name == name }
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,87 @@
|
||||||
|
package eu.kanade.tachiyomi.ui.animecategory
|
||||||
|
|
||||||
|
import android.app.Dialog
|
||||||
|
import android.os.Bundle
|
||||||
|
import com.afollestad.materialdialogs.MaterialDialog
|
||||||
|
import com.afollestad.materialdialogs.input.input
|
||||||
|
import com.bluelinelabs.conductor.Controller
|
||||||
|
import eu.kanade.tachiyomi.R
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Category
|
||||||
|
import eu.kanade.tachiyomi.ui.base.controller.DialogController
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 MaterialDialog(activity!!)
|
||||||
|
.title(R.string.action_rename_category)
|
||||||
|
.negativeButton(android.R.string.cancel)
|
||||||
|
.input(
|
||||||
|
hint = resources?.getString(R.string.name),
|
||||||
|
prefill = currentName
|
||||||
|
) { _, input ->
|
||||||
|
currentName = input.toString()
|
||||||
|
}
|
||||||
|
.positiveButton(android.R.string.ok) { onPositive() }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 companion object {
|
||||||
|
const val CATEGORY_KEY = "CategoryRenameDialog.category"
|
||||||
|
}
|
||||||
|
}
|
|
@ -88,6 +88,19 @@ class AnimelibAdapter(
|
||||||
return categories.size
|
return categories.size
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the title to display for a category.
|
||||||
|
*
|
||||||
|
* @param position the position of the element.
|
||||||
|
* @return the title to display.
|
||||||
|
*/
|
||||||
|
override fun getPageTitle(position: Int): CharSequence {
|
||||||
|
if (preferences.animeCategoryNumberOfItems().get()) {
|
||||||
|
return categories[position].let { "${it.name} (${itemsPerCategory[it.id]})" }
|
||||||
|
}
|
||||||
|
return categories[position].name
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the position of the view.
|
* Returns the position of the view.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -17,6 +17,7 @@ import com.google.android.material.tabs.TabLayout
|
||||||
import com.jakewharton.rxrelay.BehaviorRelay
|
import com.jakewharton.rxrelay.BehaviorRelay
|
||||||
import com.jakewharton.rxrelay.PublishRelay
|
import com.jakewharton.rxrelay.PublishRelay
|
||||||
import com.tfcporciuncula.flow.Preference
|
import com.tfcporciuncula.flow.Preference
|
||||||
|
import dev.chrisbanes.insetter.applyInsetter
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.animelib.AnimelibUpdateService
|
import eu.kanade.tachiyomi.data.animelib.AnimelibUpdateService
|
||||||
import eu.kanade.tachiyomi.data.database.models.Anime
|
import eu.kanade.tachiyomi.data.database.models.Anime
|
||||||
|
@ -135,7 +136,7 @@ class AnimelibController(
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateTitle() {
|
private fun updateTitle() {
|
||||||
val showCategoryTabs = preferences.categoryTabs().get()
|
val showCategoryTabs = preferences.animeCategoryTabs().get()
|
||||||
val currentCategory = adapter?.categories?.getOrNull(binding.animelibPager.currentItem)
|
val currentCategory = adapter?.categories?.getOrNull(binding.animelibPager.currentItem)
|
||||||
|
|
||||||
var title = if (showCategoryTabs) {
|
var title = if (showCategoryTabs) {
|
||||||
|
@ -144,7 +145,7 @@ class AnimelibController(
|
||||||
currentCategory?.name
|
currentCategory?.name
|
||||||
}
|
}
|
||||||
|
|
||||||
if (preferences.categoryNumberOfItems().get() && animelibAnimeRelay.hasValue()) {
|
if (preferences.animeCategoryNumberOfItems().get() && animelibAnimeRelay.hasValue()) {
|
||||||
animelibAnimeRelay.value.animes.let { animeMap ->
|
animelibAnimeRelay.value.animes.let { animeMap ->
|
||||||
if (!showCategoryTabs) {
|
if (!showCategoryTabs) {
|
||||||
title += " (${animeMap[currentCategory?.id]?.size ?: 0})"
|
title += " (${animeMap[currentCategory?.id]?.size ?: 0})"
|
||||||
|
@ -167,6 +168,12 @@ class AnimelibController(
|
||||||
override fun onViewCreated(view: View) {
|
override fun onViewCreated(view: View) {
|
||||||
super.onViewCreated(view)
|
super.onViewCreated(view)
|
||||||
|
|
||||||
|
binding.actionToolbar.applyInsetter {
|
||||||
|
type(navigationBars = true) {
|
||||||
|
margin(bottom = true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
adapter = AnimelibAdapter(this)
|
adapter = AnimelibAdapter(this)
|
||||||
binding.animelibPager.adapter = adapter
|
binding.animelibPager.adapter = adapter
|
||||||
binding.animelibPager.pageSelections()
|
binding.animelibPager.pageSelections()
|
||||||
|
@ -324,8 +331,8 @@ class AnimelibController(
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onTabsSettingsChanged() {
|
private fun onTabsSettingsChanged() {
|
||||||
tabsVisibilityRelay.call(preferences.categoryTabs().get() && adapter?.categories?.size ?: 0 > 1)
|
tabsVisibilityRelay.call(preferences.animeCategoryTabs().get() && adapter?.categories?.size ?: 0 > 1)
|
||||||
animeCountVisibilityRelay.call(preferences.categoryNumberOfItems().get())
|
animeCountVisibilityRelay.call(preferences.animeCategoryNumberOfItems().get())
|
||||||
updateTitle()
|
updateTitle()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -306,16 +306,16 @@ class AnimelibSettingsSheet(
|
||||||
override val footer = null
|
override val footer = null
|
||||||
|
|
||||||
override fun initModels() {
|
override fun initModels() {
|
||||||
showTabs.checked = preferences.categoryTabs().get()
|
showTabs.checked = preferences.animeCategoryTabs().get()
|
||||||
showNumberOfItems.checked = preferences.categoryNumberOfItems().get()
|
showNumberOfItems.checked = preferences.animeCategoryNumberOfItems().get()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onItemClicked(item: Item) {
|
override fun onItemClicked(item: Item) {
|
||||||
item as Item.CheckboxGroup
|
item as Item.CheckboxGroup
|
||||||
item.checked = !item.checked
|
item.checked = !item.checked
|
||||||
when (item) {
|
when (item) {
|
||||||
showTabs -> preferences.categoryTabs().set(item.checked)
|
showTabs -> preferences.animeCategoryTabs().set(item.checked)
|
||||||
showNumberOfItems -> preferences.categoryNumberOfItems().set(item.checked)
|
showNumberOfItems -> preferences.animeCategoryNumberOfItems().set(item.checked)
|
||||||
}
|
}
|
||||||
adapter.notifyItemChanged(item)
|
adapter.notifyItemChanged(item)
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,7 +15,6 @@ import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
|
||||||
import eu.kanade.tachiyomi.ui.category.CategoryController
|
import eu.kanade.tachiyomi.ui.category.CategoryController
|
||||||
import eu.kanade.tachiyomi.ui.download.DownloadController
|
import eu.kanade.tachiyomi.ui.download.DownloadController
|
||||||
import eu.kanade.tachiyomi.ui.recent.HistoryTabsController
|
import eu.kanade.tachiyomi.ui.recent.HistoryTabsController
|
||||||
import eu.kanade.tachiyomi.ui.setting.SettingsBackupController
|
|
||||||
import eu.kanade.tachiyomi.ui.setting.SettingsController
|
import eu.kanade.tachiyomi.ui.setting.SettingsController
|
||||||
import eu.kanade.tachiyomi.ui.setting.SettingsMainController
|
import eu.kanade.tachiyomi.ui.setting.SettingsMainController
|
||||||
import eu.kanade.tachiyomi.util.preference.add
|
import eu.kanade.tachiyomi.util.preference.add
|
||||||
|
@ -36,6 +35,7 @@ import rx.android.schedulers.AndroidSchedulers
|
||||||
import rx.subscriptions.CompositeSubscription
|
import rx.subscriptions.CompositeSubscription
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferenceKeys as Keys
|
import eu.kanade.tachiyomi.data.preference.PreferenceKeys as Keys
|
||||||
|
import eu.kanade.tachiyomi.ui.animecategory.CategoryController as AnimeCategoryController
|
||||||
|
|
||||||
class MoreController :
|
class MoreController :
|
||||||
SettingsController(),
|
SettingsController(),
|
||||||
|
@ -96,6 +96,14 @@ class MoreController :
|
||||||
router.pushController(DownloadController().withFadeTransaction())
|
router.pushController(DownloadController().withFadeTransaction())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
preference {
|
||||||
|
titleRes = R.string.anime_categories
|
||||||
|
iconRes = R.drawable.ic_label_24dp
|
||||||
|
iconTint = tintColor
|
||||||
|
onClick {
|
||||||
|
router.pushController(AnimeCategoryController().withFadeTransaction())
|
||||||
|
}
|
||||||
|
}
|
||||||
preference {
|
preference {
|
||||||
titleRes = R.string.categories
|
titleRes = R.string.categories
|
||||||
iconRes = R.drawable.ic_label_24dp
|
iconRes = R.drawable.ic_label_24dp
|
||||||
|
@ -104,14 +112,6 @@ class MoreController :
|
||||||
router.pushController(CategoryController().withFadeTransaction())
|
router.pushController(CategoryController().withFadeTransaction())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
preference {
|
|
||||||
titleRes = R.string.label_backup
|
|
||||||
iconRes = R.drawable.ic_backup_24dp
|
|
||||||
iconTint = tintColor
|
|
||||||
onClick {
|
|
||||||
router.pushController(SettingsBackupController().withFadeTransaction())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
preferenceCategory {
|
preferenceCategory {
|
||||||
|
|
|
@ -14,7 +14,9 @@
|
||||||
android:id="@+id/fast_scroller"
|
android:id="@+id/fast_scroller"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
|
android:layout_centerHorizontal="true"
|
||||||
android:layout_gravity="end"
|
android:layout_gravity="end"
|
||||||
app:fastScrollerBubbleEnabled="false" />
|
app:fastScrollerBubbleEnabled="false"
|
||||||
|
tools:visibility="visible" />
|
||||||
|
|
||||||
</eu.kanade.tachiyomi.ui.animelib.AnimelibCategoryView>
|
</eu.kanade.tachiyomi.ui.animelib.AnimelibCategoryView>
|
||||||
|
|
|
@ -4,7 +4,8 @@
|
||||||
|
|
||||||
<!--Models-->
|
<!--Models-->
|
||||||
<string name="name">Name</string>
|
<string name="name">Name</string>
|
||||||
<string name="categories">Categories</string>
|
<string name="categories">Manga Categories</string>
|
||||||
|
<string name="anime_categories">Anime Categories</string>
|
||||||
<string name="manga">Manga</string>
|
<string name="manga">Manga</string>
|
||||||
<string name="chapters">Chapters</string>
|
<string name="chapters">Chapters</string>
|
||||||
<string name="track">Tracking</string>
|
<string name="track">Tracking</string>
|
||||||
|
|
Loading…
Reference in a new issue