diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/models/Category.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/models/Category.kt index 535735e068..a63fa37b55 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/database/models/Category.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/models/Category.kt @@ -78,19 +78,19 @@ interface Category : Serializable { } companion object { - private const val DRAG_AND_DROP = 'D' - private const val ALPHA_ASC = 'a' - private const val ALPHA_DSC = 'b' - private const val UPDATED_ASC = 'c' - private const val UPDATED_DSC = 'd' - private const val UNREAD_ASC = 'e' - private const val UNREAD_DSC = 'f' - private const val LAST_READ_ASC = 'g' - private const val LAST_READ_DSC = 'h' - private const val TOTAL_ASC = 'i' - private const val TOTAL_DSC = 'j' - private const val DATE_ADDED_ASC = 'k' - private const val DATE_ADDED_DSC = 'l' + const val DRAG_AND_DROP = 'D' + const val ALPHA_ASC = 'a' + const val ALPHA_DSC = 'b' + const val UPDATED_ASC = 'c' + const val UPDATED_DSC = 'd' + const val UNREAD_ASC = 'e' + const val UNREAD_DSC = 'f' + const val LAST_READ_ASC = 'g' + const val LAST_READ_DSC = 'h' + const val TOTAL_ASC = 'i' + const val TOTAL_DSC = 'j' + const val DATE_ADDED_ASC = 'k' + const val DATE_ADDED_DSC = 'l' fun create(name: String): Category = CategoryImpl().apply { this.name = name diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryPresenter.kt index e470fc85fe..7d35d3f1a1 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryPresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryPresenter.kt @@ -72,10 +72,10 @@ class CategoryPresenter( val cat = Category.create(name) // Set the new item in the last position. - cat.order = categories.map { it.order + 1 }.max() ?: 0 + cat.order = categories.maxOf { it.order } + 1 // Insert into database. - cat.mangaSort = 'a' + cat.mangaSort = Category.ALPHA_ASC db.insertCategory(cat).executeAsBlocking() val cats = db.getCategories().executeAsBlocking() val newCat = cats.find { it.name == name } ?: return false diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/category/ManageCategoryDialog.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/category/ManageCategoryDialog.kt index 74b78398e1..056df2dca6 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/category/ManageCategoryDialog.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/category/ManageCategoryDialog.kt @@ -1,9 +1,13 @@ package eu.kanade.tachiyomi.ui.category +import android.app.Activity import android.app.Dialog import android.os.Bundle import android.widget.CompoundButton +import androidx.core.view.isVisible +import androidx.core.widget.addTextChangedListener import com.afollestad.materialdialogs.MaterialDialog +import com.afollestad.materialdialogs.callbacks.onShow import com.afollestad.materialdialogs.customview.customView import com.afollestad.materialdialogs.customview.getCustomView import com.tfcporciuncula.flow.Preference @@ -15,8 +19,6 @@ import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.getOrDefault import eu.kanade.tachiyomi.databinding.MangaCategoryDialogBinding import eu.kanade.tachiyomi.ui.base.controller.DialogController -import eu.kanade.tachiyomi.ui.library.LibraryController -import eu.kanade.tachiyomi.util.system.toast import eu.kanade.tachiyomi.util.view.gone import eu.kanade.tachiyomi.util.view.visible import eu.kanade.tachiyomi.util.view.visibleIf @@ -26,12 +28,12 @@ import uy.kohesive.injekt.injectLazy class ManageCategoryDialog(bundle: Bundle? = null) : DialogController(bundle) { - constructor(libraryController: LibraryController, category: Category) : this() { - this.libraryController = libraryController + constructor(category: Category?, updateLibrary: ((Int?) -> Unit)) : this() { + this.updateLibrary = updateLibrary this.category = category } - private var libraryController: LibraryController? = null + private var updateLibrary: ((Int?) -> Unit)? = null private var category: Category? = null private val preferences by injectLazy() @@ -39,28 +41,59 @@ class ManageCategoryDialog(bundle: Bundle? = null) : lateinit var binding: MangaCategoryDialogBinding override fun onCreateDialog(savedViewState: Bundle?): Dialog { - val dialog = MaterialDialog(activity!!).apply { - title(R.string.manage_category) - customView(viewRes = R.layout.manga_category_dialog) - negativeButton(android.R.string.cancel) - positiveButton(R.string.save) { onPositiveButtonClick() } - } + val dialog = dialog(activity!!) binding = MangaCategoryDialogBinding.bind(dialog.getCustomView()) onViewCreated() return dialog } - private fun onPositiveButtonClick() { - val category = category ?: return - if (category.id ?: 0 <= 0) return + fun dialog(activity: Activity): MaterialDialog { + return MaterialDialog(activity).apply { + title(if (category == null) R.string.new_category else R.string.manage_category) + customView(viewRes = R.layout.manga_category_dialog) + negativeButton(android.R.string.cancel) { dismiss() } + positiveButton(R.string.save) { + if (onPositiveButtonClick()) { + dismiss() + } + } + noAutoDismiss() + } + } + + fun show(activity: Activity) { + val dialog = dialog(activity) + binding = MangaCategoryDialogBinding.bind(dialog.getCustomView()) + onViewCreated() + dialog.onShow { + binding.title.requestFocus() + } + dialog.show() + } + + private fun onPositiveButtonClick(): Boolean { + if (category?.id ?: 0 <= 0 && category != null) return false val text = binding.title.text.toString() val categoryExists = categoryExists(text) - if (text.isNotBlank() && !categoryExists && !text.equals(category.name, true)) { + val category = this.category ?: Category.create(text) + if (text.isNotBlank() && !categoryExists && !text.equals(this.category?.name ?: "", true)) { category.name = text - db.insertCategory(category).executeAsBlocking() - libraryController?.presenter?.getLibrary() + if (this.category == null) { + val categories = db.getCategories().executeAsBlocking() + category.order = categories.maxOf { it.order } + 1 + category.mangaSort = Category.ALPHA_ASC + val dbCategory = db.insertCategory(category).executeAsBlocking() + category.id = dbCategory.insertedId()?.toInt() + this.category = category + } else { + db.insertCategory(category).executeAsBlocking() + } } else if (categoryExists) { - activity?.toast(R.string.category_with_name_exists) + binding.categoryTextLayout.error = binding.categoryTextLayout.context.getString(R.string.category_with_name_exists) + return false + } else { + binding.categoryTextLayout.error = binding.categoryTextLayout.context.getString(R.string.category_cannot_be_blank) + return false } if (!updatePref(preferences.downloadNewCategories(), binding.downloadNew)) { preferences.downloadNew().set(false) @@ -73,6 +106,8 @@ class ManageCategoryDialog(bundle: Bundle? = null) : preferences.libraryUpdateInterval().set(0) LibraryUpdateJob.setupTask(0) } + updateLibrary?.invoke(category.id) + return true } /** @@ -85,19 +120,22 @@ class ManageCategoryDialog(bundle: Bundle? = null) : } fun onViewCreated() { - val category = category ?: return - if (category.id ?: 0 <= 0) { + if (category?.id ?: 0 <= 0 && category != null) { binding.title.gone() binding.downloadNew.gone() binding.includeGlobal.gone() return } + binding.editCategories.isVisible = category != null binding.editCategories.setOnClickListener { router.popCurrentController() router.pushController(CategoryController().withFadeTransaction()) } - binding.title.hint = category.name - binding.title.append(category.name) + binding.title.addTextChangedListener { + binding.categoryTextLayout.error = null + } + binding.title.hint = category?.name ?: binding.editCategories.context.getString(R.string.category) + binding.title.append(category?.name ?: "") val downloadNew = preferences.downloadNew().get() setCheckbox( binding.downloadNew, diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/category/addtolibrary/AddCategoryItem.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/category/addtolibrary/AddCategoryItem.kt new file mode 100644 index 0000000000..6a9f4d7f8f --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/category/addtolibrary/AddCategoryItem.kt @@ -0,0 +1,37 @@ +package eu.kanade.tachiyomi.ui.category.addtolibrary + +import android.view.View +import com.mikepenz.fastadapter.FastAdapter +import com.mikepenz.fastadapter.items.AbstractItem +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.data.database.models.Category +import eu.kanade.tachiyomi.databinding.AddCategoryItemBinding + +class AddCategoryItem(val category: Category) : AbstractItem>() { + + /** defines the type defining this item. must be unique. preferably an id */ + override val type: Int = R.id.category_checkbox + + /** defines the layout which will be used for this item in the list */ + override val layoutRes: Int = R.layout.add_category_item + + override var identifier = category.id?.toLong() ?: -1L + + override fun getViewHolder(v: View): FastAdapter.ViewHolder { + return ViewHolder(v) + } + + class ViewHolder(view: View) : FastAdapter.ViewHolder(view) { + + val binding = AddCategoryItemBinding.bind(view) + override fun bindView(item: AddCategoryItem, payloads: List) { + binding.categoryCheckbox.text = item.category.name + binding.categoryCheckbox.isChecked = item.isSelected + } + + override fun unbindView(item: AddCategoryItem) { + binding.categoryCheckbox.text = null + binding.categoryCheckbox.isChecked = false + } + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/category/addtolibrary/SetCategoriesSheet.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/category/addtolibrary/SetCategoriesSheet.kt new file mode 100644 index 0000000000..f5efbf5154 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/category/addtolibrary/SetCategoriesSheet.kt @@ -0,0 +1,215 @@ +package eu.kanade.tachiyomi.ui.category.addtolibrary + +import android.app.Activity +import android.os.Bundle +import android.view.View +import android.view.ViewGroup +import androidx.constraintlayout.widget.ConstraintLayout +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import com.google.android.material.bottomsheet.BottomSheetBehavior +import com.google.android.material.bottomsheet.BottomSheetDialog +import com.mikepenz.fastadapter.FastAdapter +import com.mikepenz.fastadapter.ISelectionListener +import com.mikepenz.fastadapter.adapters.ItemAdapter +import com.mikepenz.fastadapter.select.SelectExtension +import com.mikepenz.fastadapter.select.getSelectExtension +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.data.database.DatabaseHelper +import eu.kanade.tachiyomi.data.database.models.Category +import eu.kanade.tachiyomi.data.database.models.Manga +import eu.kanade.tachiyomi.data.database.models.MangaCategory +import eu.kanade.tachiyomi.databinding.SetCategoriesSheetBinding +import eu.kanade.tachiyomi.ui.category.ManageCategoryDialog +import eu.kanade.tachiyomi.util.system.dpToPx +import eu.kanade.tachiyomi.util.view.expand +import eu.kanade.tachiyomi.util.view.setEdgeToEdge +import eu.kanade.tachiyomi.util.view.updateLayoutParams +import eu.kanade.tachiyomi.util.view.updatePaddingRelative +import uy.kohesive.injekt.api.get +import uy.kohesive.injekt.injectLazy +import java.util.Date +import java.util.Locale +import kotlin.math.max + +class SetCategoriesSheet( + private val activity: Activity, + private val manga: Manga, + var categories: MutableList, + var preselected: Array, + val addingToLibrary: Boolean, + val onMangaAdded: (() -> Unit) = { } +) : BottomSheetDialog +(activity, R.style.BottomSheetDialogTheme) { + + private var sheetBehavior: BottomSheetBehavior<*> + + private val fastAdapter: FastAdapter + private val itemAdapter = ItemAdapter() + private val selectExtension: SelectExtension + private val db: DatabaseHelper by injectLazy() + + private val binding = SetCategoriesSheetBinding.inflate(activity.layoutInflater) + init { + // Use activity theme for this layout + setContentView(binding.root) + + sheetBehavior = BottomSheetBehavior.from(binding.root.parent as ViewGroup) + setEdgeToEdge(activity, binding.root) + + binding.toolbarTitle.text = context.getString( + if (addingToLibrary) { + R.string.add_x_to + } else { + R.string.move_x_to + }, + manga.mangaType(context) + ) + + setOnShowListener { + updateBottomButtons() + } + sheetBehavior.addBottomSheetCallback( + object : BottomSheetBehavior.BottomSheetCallback() { + + override fun onSlide(bottomSheet: View, slideOffset: Float) { + updateBottomButtons() + } + + override fun onStateChanged(bottomSheet: View, newState: Int) { + updateBottomButtons() + } + } + ) + + binding.categoryRecyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() { + override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) { + super.onScrollStateChanged(recyclerView, newState) + if (newState == RecyclerView.SCROLL_STATE_IDLE) { + sheetBehavior.isDraggable = !recyclerView.canScrollVertically(-1) + } + } + + override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { + super.onScrolled(recyclerView, dx, dy) + if (recyclerView.canScrollVertically(-1)) { + sheetBehavior.isDraggable = false + } + } + }) + + binding.titleLayout.viewTreeObserver.addOnGlobalLayoutListener { + binding.categoryRecyclerView.updateLayoutParams { + val fullHeight = activity.window.decorView.height + val insets = activity.window.decorView.rootWindowInsets + matchConstraintMaxHeight = + fullHeight - (insets?.systemWindowInsetTop ?: 0) - + binding.titleLayout.height - binding.buttonLayout.height - 75.dpToPx + } + } + + fastAdapter = FastAdapter.with(itemAdapter) + fastAdapter.setHasStableIds(true) + binding.categoryRecyclerView.layoutManager = LinearLayoutManager(context) + binding.categoryRecyclerView.adapter = fastAdapter + itemAdapter.set(categories.map(::AddCategoryItem)) + itemAdapter.adapterItems.forEach { item -> + item.isSelected = preselected.any { it == item.category.id } + } + + selectExtension = fastAdapter.getSelectExtension() + selectExtension.apply { + isSelectable = true + multiSelect = true + setCategoriesButtons() + selectionListener = object : ISelectionListener { + override fun onSelectionChanged(item: AddCategoryItem, selected: Boolean) { + setCategoriesButtons() + } + } + } + } + + fun setCategoriesButtons() { + binding.addToCategoriesButton.text = context.getString( + if (addingToLibrary) { + R.string.add_to_ + } else { + R.string.move_to_ + }, + when (selectExtension.selections.size) { + 0 -> context.getString(R.string.default_category).lowercase(Locale.ROOT) + 1 -> selectExtension.selectedItems.firstOrNull()?.category?.name ?: "" + else -> context.resources.getQuantityString( + R.plurals.category_plural, + selectExtension.selections.size, + selectExtension.selections.size + ) + } + ) + binding.categoryRecyclerView.scrollToPosition( + max(0, itemAdapter.adapterItems.indexOf(selectExtension.selectedItems.firstOrNull())) + ) + } + + override fun onStart() { + super.onStart() + sheetBehavior.expand() + sheetBehavior.skipCollapsed = true + updateBottomButtons() + } + + fun updateBottomButtons() { + val bottomSheet = binding.root.parent as View + val bottomSheetVisibleHeight = -bottomSheet.top + (activity.window.decorView.height - bottomSheet.height) + + binding.buttonLayout.translationY = bottomSheetVisibleHeight.toFloat() + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + val attrsArray = intArrayOf(android.R.attr.actionBarSize) + val array = context.obtainStyledAttributes(attrsArray) + val headerHeight = array.getDimensionPixelSize(0, 0) + binding.buttonLayout.updatePaddingRelative( + bottom = activity.window.decorView.rootWindowInsets.systemWindowInsetBottom + ) + + binding.buttonLayout.updateLayoutParams { + height = headerHeight + binding.buttonLayout.paddingBottom + } + array.recycle() + + binding.cancelButton.setOnClickListener { dismiss() } + binding.newCategoryButton.setOnClickListener { + ManageCategoryDialog(null) { + categories = db.getCategories().executeAsBlocking() + itemAdapter.set(categories.map(::AddCategoryItem)) + itemAdapter.adapterItems.forEach { item -> + item.isSelected = it == item.category.id + } + setCategoriesButtons() + }.show(activity) + } + + binding.addToCategoriesButton.setOnClickListener { + addMangaToCategories() + dismiss() + } + } + + private fun addMangaToCategories() { + if (!manga.favorite) { + manga.favorite = !manga.favorite + + manga.date_added = Date().time + + db.insertManga(manga).executeAsBlocking() + } + + val selectedCategories = selectExtension.selectedItems.map(AddCategoryItem::category) + val mc = selectedCategories.filter { it.id != 0 }.map { MangaCategory.create(manga, it) } + db.setMangaCategories(mc, listOf(manga)) + onMangaAdded() + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/AddToLibraryCategoriesDialog.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/AddToLibraryCategoriesDialog.kt deleted file mode 100644 index 564b316296..0000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/AddToLibraryCategoriesDialog.kt +++ /dev/null @@ -1,66 +0,0 @@ -package eu.kanade.tachiyomi.ui.library - -import android.app.Dialog -import android.os.Bundle -import com.afollestad.materialdialogs.MaterialDialog -import com.afollestad.materialdialogs.callbacks.onCancel -import com.afollestad.materialdialogs.list.listItemsMultiChoice -import com.bluelinelabs.conductor.Controller -import eu.kanade.tachiyomi.R -import eu.kanade.tachiyomi.data.database.models.Category -import eu.kanade.tachiyomi.data.database.models.Manga -import eu.kanade.tachiyomi.ui.base.controller.DialogController - -/** - * This class is used when adding new manga to your library - */ -class AddToLibraryCategoriesDialog(bundle: Bundle? = null) : - DialogController(bundle) where T : Controller, T : AddToLibraryCategoriesDialog.Listener { - - private var manga: Manga? = null - - private var categories = emptyList() - - private var preselected = emptyArray() - - private var position = 0 - - constructor( - target: T, - manga: Manga, - categories: List, - preselected: Array, - position: Int = 0 - ) : this() { - - this.manga = manga - this.categories = categories - this.preselected = preselected - this.position = position - targetController = target - } - - override fun onCreateDialog(savedViewState: Bundle?): Dialog { - return MaterialDialog(activity!!).title(R.string.add_to_library).message(R.string.add_to_categories) - .listItemsMultiChoice( - items = categories.map { it.name }, - initialSelection = preselected.toIntArray(), - allowEmptySelection = true - ) { _, selections, _ -> - val newCategories = selections.map { categories[it] } - (targetController as? Listener)?.updateCategoriesForManga(manga, newCategories) - } - .positiveButton(android.R.string.ok) - .negativeButton(android.R.string.cancel) { - (targetController as? Listener)?.addToLibraryCancelled(manga, position) - } - .onCancel { - (targetController as? Listener)?.addToLibraryCancelled(manga, position) - } - } - - interface Listener { - fun updateCategoriesForManga(manga: Manga?, categories: List) - fun addToLibraryCancelled(manga: Manga?, position: Int) - } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryController.kt index 1955721bcd..f6745337ef 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryController.kt @@ -1332,7 +1332,9 @@ class LibraryController( override fun manageCategory(position: Int) { val category = (adapter.getItem(position) as? LibraryHeaderItem)?.category ?: return if (!category.isDynamic) { - ManageCategoryDialog(this, category).showDialog(router) + ManageCategoryDialog(category) { + presenter.getLibrary() + }.showDialog(router) } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaDetailsController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaDetailsController.kt index 7ae0f78774..13466052e8 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaDetailsController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaDetailsController.kt @@ -68,8 +68,6 @@ import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.ui.base.controller.BaseController import eu.kanade.tachiyomi.ui.base.controller.DialogController import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder -import eu.kanade.tachiyomi.ui.library.AddToLibraryCategoriesDialog -import eu.kanade.tachiyomi.ui.library.ChangeMangaCategoriesDialog import eu.kanade.tachiyomi.ui.library.LibraryController import eu.kanade.tachiyomi.ui.main.MainActivity import eu.kanade.tachiyomi.ui.main.SearchActivity @@ -84,6 +82,8 @@ import eu.kanade.tachiyomi.ui.security.SecureActivityDelegate import eu.kanade.tachiyomi.ui.source.BrowseController import eu.kanade.tachiyomi.ui.source.global_search.GlobalSearchController import eu.kanade.tachiyomi.ui.webview.WebViewActivity +import eu.kanade.tachiyomi.util.addOrRemoveToFavorites +import eu.kanade.tachiyomi.util.moveCategories import eu.kanade.tachiyomi.util.storage.getUriCompat import eu.kanade.tachiyomi.util.system.ThemeUtil import eu.kanade.tachiyomi.util.system.dpToPx @@ -116,9 +116,7 @@ class MangaDetailsController : FlexibleAdapter.OnItemLongClickListener, ActionMode.Callback, MangaDetailsAdapter.MangaDetailsInterface, - FlexibleAdapter.OnItemMoveListener, - ChangeMangaCategoriesDialog.Listener, - AddToLibraryCategoriesDialog.Listener { + FlexibleAdapter.OnItemMoveListener { constructor( manga: Manga?, @@ -1092,7 +1090,7 @@ class MangaDetailsController : popupView.setOnTouchListener(popup.dragToOpenListener) } - fun makeFavPopup(popupView: View, manga: Manga, categories: List): PopupMenu { + private fun makeFavPopup(popupView: View, manga: Manga, categories: List): PopupMenu { val popup = PopupMenu(view!!.context, popupView) popup.menu.add(0, 1, 0, R.string.remove_from_library) if (categories.isNotEmpty()) { @@ -1102,18 +1100,9 @@ class MangaDetailsController : // Set a listener so we are notified if a menu item is clicked popup.setOnMenuItemClickListener { menuItem -> if (menuItem.itemId == 0) { - val ids = presenter.getMangaCategoryIds() - val preselected = ids.mapNotNull { id -> - categories.indexOfFirst { it.id == id }.takeIf { it != -1 } - }.toTypedArray() - ChangeMangaCategoriesDialog( - this, - listOf(manga), - categories, - preselected - ).showDialog( - router - ) + presenter.manga.moveCategories(presenter.db, activity!!) { + updateHeader() + } } else { toggleMangaFavorite() } @@ -1123,31 +1112,24 @@ class MangaDetailsController : } private fun toggleMangaFavorite() { - if (presenter.toggleFavorite()) { - val categories = presenter.getCategories() - val defaultCategoryId = presenter.preferences.defaultCategory() - val defaultCategory = categories.find { it.id == defaultCategoryId } - when { - defaultCategory != null -> presenter.moveMangaToCategory(defaultCategory) - defaultCategoryId == 0 || categories.isEmpty() -> // 'Default' or no category - presenter.moveMangaToCategory(null) - else -> { - val ids = presenter.getMangaCategoryIds() - val preselected = ids.mapNotNull { id -> - categories.indexOfFirst { it.id == id }.takeIf { it != -1 } - }.toTypedArray() - - AddToLibraryCategoriesDialog( - this, - presenter.manga, - categories, - preselected - ).showDialog(router) - } - } - showAddedSnack() - } else { - showRemovedSnack() + val view = view ?: return + val activity = activity ?: return + snack?.dismiss() + snack = presenter.manga.addOrRemoveToFavorites( + presenter.db, + presenter.preferences, + view, + activity, + onMangaAdded = { + updateHeader() + showAddedSnack() + }, + onMangaMoved = { updateHeader() }, + onMangaDeleted = { presenter.confirmDeletion() } + ) + if (snack?.duration == Snackbar.LENGTH_INDEFINITE) { + val favButton = getHeader()?.binding?.favoriteButton + (activity as? MainActivity)?.setUndoSnackBar(snack, favButton) } } @@ -1157,43 +1139,8 @@ class MangaDetailsController : snack = view.snack(view.context.getString(R.string.added_to_library)) } - private fun showRemovedSnack() { - val view = view ?: return - snack?.dismiss() - snack = view.snack( - view.context.getString(R.string.removed_from_library), - Snackbar.LENGTH_INDEFINITE - ) { - setAction(R.string.undo) { - presenter.setFavorite(true) - } - addCallback( - object : BaseTransientBottomBar.BaseCallback() { - override fun onDismissed(transientBottomBar: Snackbar?, event: Int) { - super.onDismissed(transientBottomBar, event) - if (!presenter.manga.favorite) presenter.confirmDeletion() - } - } - ) - } - val favButton = getHeader()?.binding?.favoriteButton - (activity as? MainActivity)?.setUndoSnackBar(snack, favButton) - } - override fun mangaPresenter(): MangaDetailsPresenter = presenter - override fun updateCategoriesForMangas(mangas: List, categories: List) { - presenter.moveMangaToCategories(categories) - } - - override fun updateCategoriesForManga(manga: Manga?, categories: List) { - manga?.let { presenter.moveMangaToCategories(categories) } - } - - override fun addToLibraryCancelled(manga: Manga?, position: Int) { - manga?.let { presenter.toggleFavorite() } - } - /** * Copies a string to clipboard * diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaDetailsPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaDetailsPresenter.kt index c425bb6729..f68c1fa868 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaDetailsPresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaDetailsPresenter.kt @@ -10,7 +10,6 @@ import eu.kanade.tachiyomi.data.database.DatabaseHelper import eu.kanade.tachiyomi.data.database.models.Category import eu.kanade.tachiyomi.data.database.models.Chapter import eu.kanade.tachiyomi.data.database.models.Manga -import eu.kanade.tachiyomi.data.database.models.MangaCategory import eu.kanade.tachiyomi.data.database.models.Track import eu.kanade.tachiyomi.data.database.models.toMangaInfo import eu.kanade.tachiyomi.data.download.DownloadManager @@ -61,7 +60,7 @@ class MangaDetailsPresenter( val source: Source, val preferences: PreferencesHelper = Injekt.get(), val coverCache: CoverCache = Injekt.get(), - private val db: DatabaseHelper = Injekt.get(), + val db: DatabaseHelper = Injekt.get(), private val downloadManager: DownloadManager = Injekt.get(), private val chapterFilter: ChapterFilter = Injekt.get() ) : DownloadQueue.DownloadListener, LibraryServiceListener { @@ -334,7 +333,6 @@ class MangaDetailsPresenter( } val networkManga = nManga.await() - val mangaWasInitalized = manga.initialized if (networkManga != null) { manga.copyFrom(networkManga) manga.initialized = true @@ -405,7 +403,7 @@ class MangaDetailsPresenter( } catch (e: Exception) { withContext(Dispatchers.Main) { controller.showError(trimException(e)) } return@launch - } ?: listOf() + } isLoading = false try { syncChaptersWithSource(db, chapters, manga, source) @@ -551,38 +549,6 @@ class MangaDetailsPresenter( return db.getCategories().executeAsBlocking() } - /** - * Move the given manga to the category. - * - * @param manga the manga to move. - * @param category the selected category, or null for default category. - */ - fun moveMangaToCategory(category: Category?) { - moveMangaToCategories(listOfNotNull(category)) - } - - /** - * Move the given manga to categories. - * - * @param manga the manga to move. - * @param categories the selected categories. - */ - fun moveMangaToCategories(categories: List) { - val mc = categories.filter { it.id != 0 }.map { MangaCategory.create(manga, it) } - db.setMangaCategories(mc, listOf(manga)) - } - - /** - * Gets the category id's the manga is in, if the manga is not in a category, returns the default id. - * - * @param manga the manga to get categories from. - * @return Array of category ids the manga is in, if none returns default id - */ - fun getMangaCategoryIds(): Array { - val categories = db.getCategoriesForManga(manga).executeAsBlocking() - return categories.mapNotNull { it.id }.toTypedArray() - } - fun confirmDeletion() { coverCache.deleteFromCache(manga) db.resetMangaInfo(manga).executeAsBlocking() diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/source/browse/BrowseSourceController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/source/browse/BrowseSourceController.kt index 524f2ba113..02c7e7d7e9 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/source/browse/BrowseSourceController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/source/browse/BrowseSourceController.kt @@ -11,13 +11,11 @@ import androidx.appcompat.widget.SearchView import androidx.recyclerview.widget.DividerItemDecoration import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView -import com.google.android.material.snackbar.BaseTransientBottomBar import com.google.android.material.snackbar.Snackbar import com.jakewharton.rxbinding.support.v7.widget.queryTextChangeEvents import eu.davidea.flexibleadapter.FlexibleAdapter import eu.davidea.flexibleadapter.items.IFlexible import eu.kanade.tachiyomi.R -import eu.kanade.tachiyomi.data.database.models.Category import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.getOrDefault @@ -28,11 +26,11 @@ import eu.kanade.tachiyomi.source.model.Filter import eu.kanade.tachiyomi.source.model.FilterList import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.ui.base.controller.NucleusController -import eu.kanade.tachiyomi.ui.library.AddToLibraryCategoriesDialog import eu.kanade.tachiyomi.ui.main.MainActivity import eu.kanade.tachiyomi.ui.manga.MangaDetailsController import eu.kanade.tachiyomi.ui.source.BrowseController import eu.kanade.tachiyomi.ui.webview.WebViewActivity +import eu.kanade.tachiyomi.util.addOrRemoveToFavorites import eu.kanade.tachiyomi.util.system.connectivityManager import eu.kanade.tachiyomi.util.system.dpToPx import eu.kanade.tachiyomi.util.system.openInBrowser @@ -60,8 +58,7 @@ open class BrowseSourceController(bundle: Bundle) : NucleusController(bundle), FlexibleAdapter.OnItemClickListener, FlexibleAdapter.OnItemLongClickListener, - FlexibleAdapter.EndlessScrollListener, - AddToLibraryCategoriesDialog.Listener { + FlexibleAdapter.EndlessScrollListener { constructor( source: CatalogueSource, @@ -575,73 +572,23 @@ open class BrowseSourceController(bundle: Bundle) : */ override fun onItemLongClick(position: Int) { val manga = (adapter?.getItem(position) as? BrowseSourceItem?)?.manga ?: return + val view = view ?: return + val activity = activity ?: return snack?.dismiss() - if (manga.favorite) { - presenter.changeMangaFavorite(manga) - adapter?.notifyItemChanged(position) - snack = binding.sourceLayout.snack(R.string.removed_from_library, Snackbar.LENGTH_INDEFINITE) { - setAction(R.string.undo) { - if (!manga.favorite) addManga(manga, position) - } - addCallback( - object : BaseTransientBottomBar.BaseCallback() { - override fun onDismissed(transientBottomBar: Snackbar?, event: Int) { - super.onDismissed(transientBottomBar, event) - if (!manga.favorite) presenter.confirmDeletion(manga) - } - } - ) - } + snack = manga.addOrRemoveToFavorites( + presenter.db, + preferences, + view, + activity, + onMangaAdded = { + adapter?.notifyItemChanged(position) + snack = view.snack(R.string.added_to_library) + }, + onMangaMoved = { adapter?.notifyItemChanged(position) }, + onMangaDeleted = { presenter.confirmDeletion(manga) } + ) + if (snack?.duration == Snackbar.LENGTH_INDEFINITE) { (activity as? MainActivity)?.setUndoSnackBar(snack) - } else { - addManga(manga, position) - snack = binding.sourceLayout.snack(R.string.added_to_library) - } - } - - private fun addManga(manga: Manga, position: Int) { - presenter.changeMangaFavorite(manga) - adapter?.notifyItemChanged(position) - - val categories = presenter.getCategories() - val defaultCategoryId = preferences.defaultCategory() - val defaultCategory = categories.find { it.id == defaultCategoryId } - when { - defaultCategory != null -> presenter.moveMangaToCategory(manga, defaultCategory) - defaultCategoryId == 0 || categories.isEmpty() -> // 'Default' or no category - presenter.moveMangaToCategory(manga, null) - else -> { - val ids = presenter.getMangaCategoryIds(manga) - if (ids.isNullOrEmpty()) { - presenter.moveMangaToCategory(manga, null) - } - val preselected = ids.mapNotNull { id -> - categories.indexOfFirst { it.id == id }.takeIf { it != -1 } - }.toTypedArray() - - AddToLibraryCategoriesDialog(this, manga, categories, preselected, position) - .showDialog(router) - } - } - } - - /** - * Update manga to use selected categories. - * - * @param manga The manga to move to categories. - * @param categories The list of categories where manga will be placed. - */ - override fun updateCategoriesForManga(manga: Manga?, categories: List) { - manga?.let { presenter.updateMangaCategories(manga, categories) } - } - - /** - * Update manga to remove from favorites - */ - override fun addToLibraryCancelled(manga: Manga?, position: Int) { - manga?.let { - presenter.changeMangaFavorite(manga) - adapter?.notifyItemChanged(position) } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/source/browse/BrowseSourcePresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/source/browse/BrowseSourcePresenter.kt index a1569d4ecb..0e7d974098 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/source/browse/BrowseSourcePresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/source/browse/BrowseSourcePresenter.kt @@ -7,7 +7,6 @@ import eu.kanade.tachiyomi.data.cache.CoverCache import eu.kanade.tachiyomi.data.database.DatabaseHelper import eu.kanade.tachiyomi.data.database.models.Category import eu.kanade.tachiyomi.data.database.models.Manga -import eu.kanade.tachiyomi.data.database.models.MangaCategory import eu.kanade.tachiyomi.data.download.DownloadManager import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.source.CatalogueSource @@ -37,7 +36,6 @@ import rx.subjects.PublishSubject import timber.log.Timber import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get -import java.util.Date /** * Presenter of [BrowseSourceController]. @@ -45,7 +43,7 @@ import java.util.Date open class BrowseSourcePresenter( sourceId: Long, sourceManager: SourceManager = Injekt.get(), - private val db: DatabaseHelper = Injekt.get(), + val db: DatabaseHelper = Injekt.get(), private val prefs: PreferencesHelper = Injekt.get(), private val coverCache: CoverCache = Injekt.get() ) : BasePresenter() { @@ -278,22 +276,6 @@ open class BrowseSourcePresenter( .onErrorResumeNext { Observable.just(manga) } } - /** - * Adds or removes a manga from the library. - * - * @param manga the manga to update. - */ - fun changeMangaFavorite(manga: Manga) { - manga.favorite = !manga.favorite - - when (manga.favorite) { - true -> manga.date_added = Date().time - false -> manga.date_added = 0 - } - - db.insertManga(manga).executeAsBlocking() - } - fun confirmDeletion(manga: Manga) { coverCache.deleteFromCache(manga) val downloadManager: DownloadManager = Injekt.get() @@ -365,56 +347,4 @@ open class BrowseSourcePresenter( fun getCategories(): List { return db.getCategories().executeAsBlocking() } - - /** - * Gets the category id's the manga is in, if the manga is not in a category, returns the default id. - * - * @param manga the manga to get categories from. - * @return Array of category ids the manga is in, if none returns default id - */ - fun getMangaCategoryIds(manga: Manga): Array { - val categories = db.getCategoriesForManga(manga).executeAsBlocking() - return categories.mapNotNull { it.id }.toTypedArray() - } - - /** - * Move the given manga to categories. - * - * @param categories the selected categories. - * @param manga the manga to move. - */ - private fun moveMangaToCategories(manga: Manga, categories: List) { - val mc = categories.filter { it.id != 0 }.map { MangaCategory.create(manga, it) } - db.setMangaCategories(mc, listOf(manga)) - } - - /** - * Move the given manga to the category. - * - * @param category the selected category. - * @param manga the manga to move. - */ - fun moveMangaToCategory(manga: Manga, category: Category?) { - moveMangaToCategories(manga, listOfNotNull(category)) - } - - /** - * Update manga to use selected categories. - * - * @param manga needed to change - * @param selectedCategories selected categories - */ - fun updateMangaCategories(manga: Manga, selectedCategories: List) { - if (selectedCategories.isNotEmpty()) { - if (!manga.favorite) { - changeMangaFavorite(manga) - } - - moveMangaToCategories(manga, selectedCategories.filter { it.id != 0 }) - } else { - if (!manga.favorite) { - changeMangaFavorite(manga) - } - } - } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/MangaExtensions.kt b/app/src/main/java/eu/kanade/tachiyomi/util/MangaExtensions.kt index 2bd26e444b..1e1a7a84c5 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/util/MangaExtensions.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/util/MangaExtensions.kt @@ -1,9 +1,18 @@ package eu.kanade.tachiyomi.util +import android.app.Activity +import android.view.View +import com.google.android.material.snackbar.BaseTransientBottomBar +import com.google.android.material.snackbar.Snackbar +import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.database.DatabaseHelper import eu.kanade.tachiyomi.data.database.models.Manga +import eu.kanade.tachiyomi.data.database.models.MangaCategory import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.source.LocalSource +import eu.kanade.tachiyomi.ui.category.addtolibrary.SetCategoriesSheet +import eu.kanade.tachiyomi.util.view.snack +import java.util.Date fun Manga.isLocal() = source == LocalSource.ID @@ -25,3 +34,108 @@ fun Manga.shouldDownloadNewChapters(db: DatabaseHelper, prefs: PreferencesHelper return categoriesForManga.intersect(categoriesToDownload).isNotEmpty() } + +fun Manga.moveCategories( + db: DatabaseHelper, + activity: Activity, + onMangaMoved: () -> Unit +) { + val categories = db.getCategories().executeAsBlocking() + val categoriesForManga = db.getCategoriesForManga(this).executeAsBlocking() + val ids = categoriesForManga.mapNotNull { it.id }.toTypedArray() + SetCategoriesSheet( + activity, + this, + categories.toMutableList(), + ids, + false + ) { + onMangaMoved() + }.show() +} + +fun Manga.addOrRemoveToFavorites( + db: DatabaseHelper, + preferences: PreferencesHelper, + view: View, + activity: Activity, + onMangaAdded: () -> Unit, + onMangaMoved: () -> Unit, + onMangaDeleted: () -> Unit +): Snackbar? { + if (!favorite) { + val categories = db.getCategories().executeAsBlocking() + val defaultCategoryId = preferences.defaultCategory() + val defaultCategory = categories.find { it.id == defaultCategoryId } + when { + defaultCategory != null -> { + favorite = true + date_added = Date().time + db.insertManga(this).executeAsBlocking() + val mc = MangaCategory.create(this, defaultCategory) + db.setMangaCategories(listOf(mc), listOf(this)) + onMangaMoved() + return view.snack(activity.getString(R.string.added_to_, defaultCategory.name)) { + setAction(R.string.change) { + moveCategories(db, activity, onMangaMoved) + } + } + } + defaultCategoryId == 0 || categories.isEmpty() -> { // 'Default' or no category + favorite = true + date_added = Date().time + db.insertManga(this).executeAsBlocking() + db.setMangaCategories(emptyList(), listOf(this)) + onMangaMoved() + return if (categories.isNotEmpty()) { + view.snack(activity.getString(R.string.added_to_, activity.getString(R.string.default_value))) { + setAction(R.string.change) { + moveCategories(db, activity, onMangaMoved) + } + } + } else { + view.snack(R.string.added_to_library) + } + } + else -> { + val categoriesForManga = db.getCategoriesForManga(this).executeAsBlocking() + val ids = categoriesForManga.mapNotNull { it.id }.toTypedArray() + + SetCategoriesSheet( + activity, + this, + categories.toMutableList(), + ids, + true + ) { + onMangaAdded() + }.show() + } + } + } else { + val lastAddedDate = date_added + favorite = false + date_added = 0 + db.insertManga(this).executeAsBlocking() + onMangaMoved() + return view.snack(view.context.getString(R.string.removed_from_library), Snackbar.LENGTH_INDEFINITE) { + setAction(R.string.undo) { + favorite = true + date_added = lastAddedDate + db.insertManga(this@addOrRemoveToFavorites).executeAsBlocking() + onMangaMoved() + } + addCallback( + object : BaseTransientBottomBar.BaseCallback() { + override fun onDismissed(transientBottomBar: Snackbar?, event: Int) { + super.onDismissed(transientBottomBar, event) + if (!favorite) { + onMangaDeleted() + } + } + } + ) + } + } + return null +} diff --git a/app/src/main/res/drawable/ic_plus_24dp.xml b/app/src/main/res/drawable/ic_plus_24dp.xml new file mode 100644 index 0000000000..fe500883a4 --- /dev/null +++ b/app/src/main/res/drawable/ic_plus_24dp.xml @@ -0,0 +1,8 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/add_category_item.xml b/app/src/main/res/layout/add_category_item.xml new file mode 100644 index 0000000000..52dd83492a --- /dev/null +++ b/app/src/main/res/layout/add_category_item.xml @@ -0,0 +1,12 @@ + + diff --git a/app/src/main/res/layout/categories_item.xml b/app/src/main/res/layout/categories_item.xml index 5b4a67c760..ee092b6ce0 100644 --- a/app/src/main/res/layout/categories_item.xml +++ b/app/src/main/res/layout/categories_item.xml @@ -50,7 +50,7 @@ android:layout_height="match_parent" android:background="@null" android:imeOptions="actionDone" - android:inputType="none" + android:inputType="textCapSentences" android:maxLines="1" android:singleLine="true" android:textColor="@color/textColorPrimary" diff --git a/app/src/main/res/layout/manga_category_dialog.xml b/app/src/main/res/layout/manga_category_dialog.xml index 86f16b41ef..d71b03bbdc 100644 --- a/app/src/main/res/layout/manga_category_dialog.xml +++ b/app/src/main/res/layout/manga_category_dialog.xml @@ -2,17 +2,30 @@ - + android:layout_marginEnd="16dp" + android:hint="@string/title" + app:boxStrokeColor="@color/colorAccent" + app:endIconMode="clear_text" + app:hintEnabled="false" + app:hintTextColor="@color/colorAccent"> + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 46f2cb0362..8d6fa5ebaa 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -21,6 +21,8 @@ Author Artist Description + Move %1$s to… + Add %1$s to… Ongoing @@ -69,6 +71,7 @@ Adding %1$s to update queue %1$s is already in queue Create new category + New category Category is empty Category is hidden Top category (%1$s) @@ -86,12 +89,12 @@ Manage category Rename category Move to categories - Choose which categories to add this to. If none are selected, this will be added to the "default" category %d category %d categories A category with that name already exists! + Category name cannot be blank Category deleted Press and hold to edit a category Jump to category @@ -788,6 +791,8 @@ Add + Add to %1$s + Added to %1$s All Alphabetically Always @@ -801,6 +806,7 @@ Report a Bug Cancel Center + Change Charging Clear Close @@ -839,6 +845,7 @@ More Move to bottom Move to top + Move to %1$s Moved to %1$s Never Newest