From 9f1f025def287e25e500432cdb4941ba0a9e82b0 Mon Sep 17 00:00:00 2001 From: jmir1 Date: Thu, 27 May 2021 02:15:24 +0200 Subject: [PATCH] add anime categories --- .../data/preference/PreferenceKeys.kt | 4 + .../data/preference/PreferencesHelper.kt | 4 + .../ui/animecategory/CategoryAdapter.kt | 42 +++ .../ui/animecategory/CategoryController.kt | 350 ++++++++++++++++++ .../ui/animecategory/CategoryCreateDialog.kt | 50 +++ .../ui/animecategory/CategoryHolder.kt | 40 ++ .../ui/animecategory/CategoryItem.kt | 73 ++++ .../ui/animecategory/CategoryPresenter.kt | 106 ++++++ .../ui/animecategory/CategoryRenameDialog.kt | 87 +++++ .../tachiyomi/ui/animelib/AnimelibAdapter.kt | 13 + .../ui/animelib/AnimelibController.kt | 15 +- .../ui/animelib/AnimelibSettingsSheet.kt | 8 +- .../tachiyomi/ui/more/MoreController.kt | 18 +- app/src/main/res/layout/animelib_category.xml | 4 +- app/src/main/res/values/strings.xml | 3 +- 15 files changed, 798 insertions(+), 19 deletions(-) create mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/animecategory/CategoryAdapter.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/animecategory/CategoryController.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/animecategory/CategoryCreateDialog.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/animecategory/CategoryHolder.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/animecategory/CategoryItem.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/animecategory/CategoryPresenter.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/animecategory/CategoryRenameDialog.kt diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferenceKeys.kt b/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferenceKeys.kt index a25b4cbcb..1842863f6 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferenceKeys.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferenceKeys.kt @@ -224,8 +224,12 @@ object PreferenceKeys { const val categoryTabs = "display_category_tabs" + const val animeCategoryTabs = "display_anime_category_tabs" + const val categoryNumberOfItems = "display_number_of_items" + const val animeCategoryNumberOfItems = "display_number_of_items_anime" + const val alwaysShowChapterTransition = "always_show_chapter_transition" const val searchPinnedSourcesOnly = "search_pinned_sources_only" diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt b/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt index 3ed5f77fa..7ae18abd7 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt @@ -272,8 +272,12 @@ class PreferencesHelper(val context: Context) { fun categoryTabs() = flowPrefs.getBoolean(Keys.categoryTabs, true) + fun animeCategoryTabs() = flowPrefs.getBoolean(Keys.animeCategoryTabs, true) + 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 filterUnread() = flowPrefs.getInt(Keys.filterUnread, ExtendedNavigationView.Item.TriStateGroup.State.IGNORE.value) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/animecategory/CategoryAdapter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/animecategory/CategoryAdapter.kt new file mode 100644 index 000000000..cf54295be --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/animecategory/CategoryAdapter.kt @@ -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(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) + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/animecategory/CategoryController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/animecategory/CategoryController.kt new file mode 100644 index 000000000..fb4f61d21 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/animecategory/CategoryController.kt @@ -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(), + 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) { + 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?) { + 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) + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/animecategory/CategoryCreateDialog.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/animecategory/CategoryCreateDialog.kt new file mode 100644 index 000000000..3aa75a660 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/animecategory/CategoryCreateDialog.kt @@ -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(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) + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/animecategory/CategoryHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/animecategory/CategoryHolder.kt new file mode 100644 index 000000000..0bd13b88c --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/animecategory/CategoryHolder.kt @@ -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) + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/animecategory/CategoryItem.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/animecategory/CategoryItem.kt new file mode 100644 index 000000000..d7564a834 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/animecategory/CategoryItem.kt @@ -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() { + + /** + * 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>): 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>, + holder: CategoryHolder, + position: Int, + payloads: List? + ) { + 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!! + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/animecategory/CategoryPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/animecategory/CategoryPresenter.kt new file mode 100644 index 000000000..3d40452b8 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/animecategory/CategoryPresenter.kt @@ -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() { + + /** + * List containing categories. + */ + private var categories: List = 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) { + db.deleteCategories(categories).asRxObservable().subscribe() + } + + /** + * Reorders the given categories in the database. + * + * @param categories The list of categories to reorder. + */ + fun reorderCategories(categories: List) { + 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 } + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/animecategory/CategoryRenameDialog.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/animecategory/CategoryRenameDialog.kt new file mode 100644 index 000000000..687be519a --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/animecategory/CategoryRenameDialog.kt @@ -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(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" + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/animelib/AnimelibAdapter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/animelib/AnimelibAdapter.kt index d650b03dd..21489fef8 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/animelib/AnimelibAdapter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/animelib/AnimelibAdapter.kt @@ -88,6 +88,19 @@ class AnimelibAdapter( 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. */ diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/animelib/AnimelibController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/animelib/AnimelibController.kt index 0770d574d..e712f80cf 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/animelib/AnimelibController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/animelib/AnimelibController.kt @@ -17,6 +17,7 @@ import com.google.android.material.tabs.TabLayout import com.jakewharton.rxrelay.BehaviorRelay import com.jakewharton.rxrelay.PublishRelay import com.tfcporciuncula.flow.Preference +import dev.chrisbanes.insetter.applyInsetter import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.animelib.AnimelibUpdateService import eu.kanade.tachiyomi.data.database.models.Anime @@ -135,7 +136,7 @@ class AnimelibController( } private fun updateTitle() { - val showCategoryTabs = preferences.categoryTabs().get() + val showCategoryTabs = preferences.animeCategoryTabs().get() val currentCategory = adapter?.categories?.getOrNull(binding.animelibPager.currentItem) var title = if (showCategoryTabs) { @@ -144,7 +145,7 @@ class AnimelibController( currentCategory?.name } - if (preferences.categoryNumberOfItems().get() && animelibAnimeRelay.hasValue()) { + if (preferences.animeCategoryNumberOfItems().get() && animelibAnimeRelay.hasValue()) { animelibAnimeRelay.value.animes.let { animeMap -> if (!showCategoryTabs) { title += " (${animeMap[currentCategory?.id]?.size ?: 0})" @@ -167,6 +168,12 @@ class AnimelibController( override fun onViewCreated(view: View) { super.onViewCreated(view) + binding.actionToolbar.applyInsetter { + type(navigationBars = true) { + margin(bottom = true) + } + } + adapter = AnimelibAdapter(this) binding.animelibPager.adapter = adapter binding.animelibPager.pageSelections() @@ -324,8 +331,8 @@ class AnimelibController( } private fun onTabsSettingsChanged() { - tabsVisibilityRelay.call(preferences.categoryTabs().get() && adapter?.categories?.size ?: 0 > 1) - animeCountVisibilityRelay.call(preferences.categoryNumberOfItems().get()) + tabsVisibilityRelay.call(preferences.animeCategoryTabs().get() && adapter?.categories?.size ?: 0 > 1) + animeCountVisibilityRelay.call(preferences.animeCategoryNumberOfItems().get()) updateTitle() } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/animelib/AnimelibSettingsSheet.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/animelib/AnimelibSettingsSheet.kt index 905b594f6..062095160 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/animelib/AnimelibSettingsSheet.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/animelib/AnimelibSettingsSheet.kt @@ -306,16 +306,16 @@ class AnimelibSettingsSheet( override val footer = null override fun initModels() { - showTabs.checked = preferences.categoryTabs().get() - showNumberOfItems.checked = preferences.categoryNumberOfItems().get() + showTabs.checked = preferences.animeCategoryTabs().get() + showNumberOfItems.checked = preferences.animeCategoryNumberOfItems().get() } override fun onItemClicked(item: Item) { item as Item.CheckboxGroup item.checked = !item.checked when (item) { - showTabs -> preferences.categoryTabs().set(item.checked) - showNumberOfItems -> preferences.categoryNumberOfItems().set(item.checked) + showTabs -> preferences.animeCategoryTabs().set(item.checked) + showNumberOfItems -> preferences.animeCategoryNumberOfItems().set(item.checked) } adapter.notifyItemChanged(item) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/more/MoreController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/more/MoreController.kt index 22c5777d6..fc39e0eba 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/more/MoreController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/more/MoreController.kt @@ -15,7 +15,6 @@ import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction import eu.kanade.tachiyomi.ui.category.CategoryController import eu.kanade.tachiyomi.ui.download.DownloadController 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.SettingsMainController import eu.kanade.tachiyomi.util.preference.add @@ -36,6 +35,7 @@ import rx.android.schedulers.AndroidSchedulers import rx.subscriptions.CompositeSubscription import uy.kohesive.injekt.injectLazy import eu.kanade.tachiyomi.data.preference.PreferenceKeys as Keys +import eu.kanade.tachiyomi.ui.animecategory.CategoryController as AnimeCategoryController class MoreController : SettingsController(), @@ -96,6 +96,14 @@ class MoreController : router.pushController(DownloadController().withFadeTransaction()) } } + preference { + titleRes = R.string.anime_categories + iconRes = R.drawable.ic_label_24dp + iconTint = tintColor + onClick { + router.pushController(AnimeCategoryController().withFadeTransaction()) + } + } preference { titleRes = R.string.categories iconRes = R.drawable.ic_label_24dp @@ -104,14 +112,6 @@ class MoreController : router.pushController(CategoryController().withFadeTransaction()) } } - preference { - titleRes = R.string.label_backup - iconRes = R.drawable.ic_backup_24dp - iconTint = tintColor - onClick { - router.pushController(SettingsBackupController().withFadeTransaction()) - } - } } preferenceCategory { diff --git a/app/src/main/res/layout/animelib_category.xml b/app/src/main/res/layout/animelib_category.xml index 15f7a3324..1bc262eb1 100644 --- a/app/src/main/res/layout/animelib_category.xml +++ b/app/src/main/res/layout/animelib_category.xml @@ -14,7 +14,9 @@ android:id="@+id/fast_scroller" android:layout_width="wrap_content" android:layout_height="match_parent" + android:layout_centerHorizontal="true" android:layout_gravity="end" - app:fastScrollerBubbleEnabled="false" /> + app:fastScrollerBubbleEnabled="false" + tools:visibility="visible" /> diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index bc60271cb..86dff7d45 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -4,7 +4,8 @@ Name - Categories + Manga Categories + Anime Categories Manga Chapters Tracking