Changing Add to/Change in library dialog to a bottom sheet

I just don't get tired of them.

This also has a button to add a new category

Also updating the snackbar when adding to library to show a "change" button to update the categories. In case you have a default category set
This commit is contained in:
Jays2Kings 2021-04-05 21:07:59 -04:00
parent 6d6766a86a
commit 0e62516777
18 changed files with 668 additions and 367 deletions

View File

@ -78,19 +78,19 @@ interface Category : Serializable {
} }
companion object { companion object {
private const val DRAG_AND_DROP = 'D' const val DRAG_AND_DROP = 'D'
private const val ALPHA_ASC = 'a' const val ALPHA_ASC = 'a'
private const val ALPHA_DSC = 'b' const val ALPHA_DSC = 'b'
private const val UPDATED_ASC = 'c' const val UPDATED_ASC = 'c'
private const val UPDATED_DSC = 'd' const val UPDATED_DSC = 'd'
private const val UNREAD_ASC = 'e' const val UNREAD_ASC = 'e'
private const val UNREAD_DSC = 'f' const val UNREAD_DSC = 'f'
private const val LAST_READ_ASC = 'g' const val LAST_READ_ASC = 'g'
private const val LAST_READ_DSC = 'h' const val LAST_READ_DSC = 'h'
private const val TOTAL_ASC = 'i' const val TOTAL_ASC = 'i'
private const val TOTAL_DSC = 'j' const val TOTAL_DSC = 'j'
private const val DATE_ADDED_ASC = 'k' const val DATE_ADDED_ASC = 'k'
private const val DATE_ADDED_DSC = 'l' const val DATE_ADDED_DSC = 'l'
fun create(name: String): Category = CategoryImpl().apply { fun create(name: String): Category = CategoryImpl().apply {
this.name = name this.name = name

View File

@ -72,10 +72,10 @@ class CategoryPresenter(
val cat = Category.create(name) val cat = Category.create(name)
// Set the new item in the last position. // 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. // Insert into database.
cat.mangaSort = 'a' cat.mangaSort = Category.ALPHA_ASC
db.insertCategory(cat).executeAsBlocking() db.insertCategory(cat).executeAsBlocking()
val cats = db.getCategories().executeAsBlocking() val cats = db.getCategories().executeAsBlocking()
val newCat = cats.find { it.name == name } ?: return false val newCat = cats.find { it.name == name } ?: return false

View File

@ -1,9 +1,13 @@
package eu.kanade.tachiyomi.ui.category package eu.kanade.tachiyomi.ui.category
import android.app.Activity
import android.app.Dialog import android.app.Dialog
import android.os.Bundle import android.os.Bundle
import android.widget.CompoundButton import android.widget.CompoundButton
import androidx.core.view.isVisible
import androidx.core.widget.addTextChangedListener
import com.afollestad.materialdialogs.MaterialDialog import com.afollestad.materialdialogs.MaterialDialog
import com.afollestad.materialdialogs.callbacks.onShow
import com.afollestad.materialdialogs.customview.customView import com.afollestad.materialdialogs.customview.customView
import com.afollestad.materialdialogs.customview.getCustomView import com.afollestad.materialdialogs.customview.getCustomView
import com.tfcporciuncula.flow.Preference 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.data.preference.getOrDefault
import eu.kanade.tachiyomi.databinding.MangaCategoryDialogBinding import eu.kanade.tachiyomi.databinding.MangaCategoryDialogBinding
import eu.kanade.tachiyomi.ui.base.controller.DialogController 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.gone
import eu.kanade.tachiyomi.util.view.visible import eu.kanade.tachiyomi.util.view.visible
import eu.kanade.tachiyomi.util.view.visibleIf import eu.kanade.tachiyomi.util.view.visibleIf
@ -26,12 +28,12 @@ import uy.kohesive.injekt.injectLazy
class ManageCategoryDialog(bundle: Bundle? = null) : class ManageCategoryDialog(bundle: Bundle? = null) :
DialogController(bundle) { DialogController(bundle) {
constructor(libraryController: LibraryController, category: Category) : this() { constructor(category: Category?, updateLibrary: ((Int?) -> Unit)) : this() {
this.libraryController = libraryController this.updateLibrary = updateLibrary
this.category = category this.category = category
} }
private var libraryController: LibraryController? = null private var updateLibrary: ((Int?) -> Unit)? = null
private var category: Category? = null private var category: Category? = null
private val preferences by injectLazy<PreferencesHelper>() private val preferences by injectLazy<PreferencesHelper>()
@ -39,28 +41,59 @@ class ManageCategoryDialog(bundle: Bundle? = null) :
lateinit var binding: MangaCategoryDialogBinding lateinit var binding: MangaCategoryDialogBinding
override fun onCreateDialog(savedViewState: Bundle?): Dialog { override fun onCreateDialog(savedViewState: Bundle?): Dialog {
val dialog = MaterialDialog(activity!!).apply { val dialog = dialog(activity!!)
title(R.string.manage_category)
customView(viewRes = R.layout.manga_category_dialog)
negativeButton(android.R.string.cancel)
positiveButton(R.string.save) { onPositiveButtonClick() }
}
binding = MangaCategoryDialogBinding.bind(dialog.getCustomView()) binding = MangaCategoryDialogBinding.bind(dialog.getCustomView())
onViewCreated() onViewCreated()
return dialog return dialog
} }
private fun onPositiveButtonClick() { fun dialog(activity: Activity): MaterialDialog {
val category = category ?: return return MaterialDialog(activity).apply {
if (category.id ?: 0 <= 0) return 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 text = binding.title.text.toString()
val categoryExists = categoryExists(text) 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 category.name = text
db.insertCategory(category).executeAsBlocking() if (this.category == null) {
libraryController?.presenter?.getLibrary() 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) { } 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)) { if (!updatePref(preferences.downloadNewCategories(), binding.downloadNew)) {
preferences.downloadNew().set(false) preferences.downloadNew().set(false)
@ -73,6 +106,8 @@ class ManageCategoryDialog(bundle: Bundle? = null) :
preferences.libraryUpdateInterval().set(0) preferences.libraryUpdateInterval().set(0)
LibraryUpdateJob.setupTask(0) LibraryUpdateJob.setupTask(0)
} }
updateLibrary?.invoke(category.id)
return true
} }
/** /**
@ -85,19 +120,22 @@ class ManageCategoryDialog(bundle: Bundle? = null) :
} }
fun onViewCreated() { fun onViewCreated() {
val category = category ?: return if (category?.id ?: 0 <= 0 && category != null) {
if (category.id ?: 0 <= 0) {
binding.title.gone() binding.title.gone()
binding.downloadNew.gone() binding.downloadNew.gone()
binding.includeGlobal.gone() binding.includeGlobal.gone()
return return
} }
binding.editCategories.isVisible = category != null
binding.editCategories.setOnClickListener { binding.editCategories.setOnClickListener {
router.popCurrentController() router.popCurrentController()
router.pushController(CategoryController().withFadeTransaction()) router.pushController(CategoryController().withFadeTransaction())
} }
binding.title.hint = category.name binding.title.addTextChangedListener {
binding.title.append(category.name) 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() val downloadNew = preferences.downloadNew().get()
setCheckbox( setCheckbox(
binding.downloadNew, binding.downloadNew,

View File

@ -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<FastAdapter.ViewHolder<AddCategoryItem>>() {
/** 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<AddCategoryItem> {
return ViewHolder(v)
}
class ViewHolder(view: View) : FastAdapter.ViewHolder<AddCategoryItem>(view) {
val binding = AddCategoryItemBinding.bind(view)
override fun bindView(item: AddCategoryItem, payloads: List<Any>) {
binding.categoryCheckbox.text = item.category.name
binding.categoryCheckbox.isChecked = item.isSelected
}
override fun unbindView(item: AddCategoryItem) {
binding.categoryCheckbox.text = null
binding.categoryCheckbox.isChecked = false
}
}
}

View File

@ -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<Category>,
var preselected: Array<Int>,
val addingToLibrary: Boolean,
val onMangaAdded: (() -> Unit) = { }
) : BottomSheetDialog
(activity, R.style.BottomSheetDialogTheme) {
private var sheetBehavior: BottomSheetBehavior<*>
private val fastAdapter: FastAdapter<AddCategoryItem>
private val itemAdapter = ItemAdapter<AddCategoryItem>()
private val selectExtension: SelectExtension<AddCategoryItem>
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<ConstraintLayout.LayoutParams> {
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<AddCategoryItem> {
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<ConstraintLayout.LayoutParams> {
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()
}
}

View File

@ -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<T>(bundle: Bundle? = null) :
DialogController(bundle) where T : Controller, T : AddToLibraryCategoriesDialog.Listener {
private var manga: Manga? = null
private var categories = emptyList<Category>()
private var preselected = emptyArray<Int>()
private var position = 0
constructor(
target: T,
manga: Manga,
categories: List<Category>,
preselected: Array<Int>,
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<Category>)
fun addToLibraryCancelled(manga: Manga?, position: Int)
}
}

View File

@ -1332,7 +1332,9 @@ class LibraryController(
override fun manageCategory(position: Int) { override fun manageCategory(position: Int) {
val category = (adapter.getItem(position) as? LibraryHeaderItem)?.category ?: return val category = (adapter.getItem(position) as? LibraryHeaderItem)?.category ?: return
if (!category.isDynamic) { if (!category.isDynamic) {
ManageCategoryDialog(this, category).showDialog(router) ManageCategoryDialog(category) {
presenter.getLibrary()
}.showDialog(router)
} }
} }

View File

@ -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.BaseController
import eu.kanade.tachiyomi.ui.base.controller.DialogController import eu.kanade.tachiyomi.ui.base.controller.DialogController
import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder 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.library.LibraryController
import eu.kanade.tachiyomi.ui.main.MainActivity import eu.kanade.tachiyomi.ui.main.MainActivity
import eu.kanade.tachiyomi.ui.main.SearchActivity 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.BrowseController
import eu.kanade.tachiyomi.ui.source.global_search.GlobalSearchController import eu.kanade.tachiyomi.ui.source.global_search.GlobalSearchController
import eu.kanade.tachiyomi.ui.webview.WebViewActivity 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.storage.getUriCompat
import eu.kanade.tachiyomi.util.system.ThemeUtil import eu.kanade.tachiyomi.util.system.ThemeUtil
import eu.kanade.tachiyomi.util.system.dpToPx import eu.kanade.tachiyomi.util.system.dpToPx
@ -116,9 +116,7 @@ class MangaDetailsController :
FlexibleAdapter.OnItemLongClickListener, FlexibleAdapter.OnItemLongClickListener,
ActionMode.Callback, ActionMode.Callback,
MangaDetailsAdapter.MangaDetailsInterface, MangaDetailsAdapter.MangaDetailsInterface,
FlexibleAdapter.OnItemMoveListener, FlexibleAdapter.OnItemMoveListener {
ChangeMangaCategoriesDialog.Listener,
AddToLibraryCategoriesDialog.Listener {
constructor( constructor(
manga: Manga?, manga: Manga?,
@ -1092,7 +1090,7 @@ class MangaDetailsController :
popupView.setOnTouchListener(popup.dragToOpenListener) popupView.setOnTouchListener(popup.dragToOpenListener)
} }
fun makeFavPopup(popupView: View, manga: Manga, categories: List<Category>): PopupMenu { private fun makeFavPopup(popupView: View, manga: Manga, categories: List<Category>): PopupMenu {
val popup = PopupMenu(view!!.context, popupView) val popup = PopupMenu(view!!.context, popupView)
popup.menu.add(0, 1, 0, R.string.remove_from_library) popup.menu.add(0, 1, 0, R.string.remove_from_library)
if (categories.isNotEmpty()) { if (categories.isNotEmpty()) {
@ -1102,18 +1100,9 @@ class MangaDetailsController :
// Set a listener so we are notified if a menu item is clicked // Set a listener so we are notified if a menu item is clicked
popup.setOnMenuItemClickListener { menuItem -> popup.setOnMenuItemClickListener { menuItem ->
if (menuItem.itemId == 0) { if (menuItem.itemId == 0) {
val ids = presenter.getMangaCategoryIds() presenter.manga.moveCategories(presenter.db, activity!!) {
val preselected = ids.mapNotNull { id -> updateHeader()
categories.indexOfFirst { it.id == id }.takeIf { it != -1 } }
}.toTypedArray()
ChangeMangaCategoriesDialog(
this,
listOf(manga),
categories,
preselected
).showDialog(
router
)
} else { } else {
toggleMangaFavorite() toggleMangaFavorite()
} }
@ -1123,31 +1112,24 @@ class MangaDetailsController :
} }
private fun toggleMangaFavorite() { private fun toggleMangaFavorite() {
if (presenter.toggleFavorite()) { val view = view ?: return
val categories = presenter.getCategories() val activity = activity ?: return
val defaultCategoryId = presenter.preferences.defaultCategory() snack?.dismiss()
val defaultCategory = categories.find { it.id == defaultCategoryId } snack = presenter.manga.addOrRemoveToFavorites(
when { presenter.db,
defaultCategory != null -> presenter.moveMangaToCategory(defaultCategory) presenter.preferences,
defaultCategoryId == 0 || categories.isEmpty() -> // 'Default' or no category view,
presenter.moveMangaToCategory(null) activity,
else -> { onMangaAdded = {
val ids = presenter.getMangaCategoryIds() updateHeader()
val preselected = ids.mapNotNull { id -> showAddedSnack()
categories.indexOfFirst { it.id == id }.takeIf { it != -1 } },
}.toTypedArray() onMangaMoved = { updateHeader() },
onMangaDeleted = { presenter.confirmDeletion() }
AddToLibraryCategoriesDialog( )
this, if (snack?.duration == Snackbar.LENGTH_INDEFINITE) {
presenter.manga, val favButton = getHeader()?.binding?.favoriteButton
categories, (activity as? MainActivity)?.setUndoSnackBar(snack, favButton)
preselected
).showDialog(router)
}
}
showAddedSnack()
} else {
showRemovedSnack()
} }
} }
@ -1157,43 +1139,8 @@ class MangaDetailsController :
snack = view.snack(view.context.getString(R.string.added_to_library)) 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<Snackbar>() {
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 mangaPresenter(): MangaDetailsPresenter = presenter
override fun updateCategoriesForMangas(mangas: List<Manga>, categories: List<Category>) {
presenter.moveMangaToCategories(categories)
}
override fun updateCategoriesForManga(manga: Manga?, categories: List<Category>) {
manga?.let { presenter.moveMangaToCategories(categories) }
}
override fun addToLibraryCancelled(manga: Manga?, position: Int) {
manga?.let { presenter.toggleFavorite() }
}
/** /**
* Copies a string to clipboard * Copies a string to clipboard
* *

View File

@ -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.Category
import eu.kanade.tachiyomi.data.database.models.Chapter import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.data.database.models.Manga 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.Track
import eu.kanade.tachiyomi.data.database.models.toMangaInfo import eu.kanade.tachiyomi.data.database.models.toMangaInfo
import eu.kanade.tachiyomi.data.download.DownloadManager import eu.kanade.tachiyomi.data.download.DownloadManager
@ -61,7 +60,7 @@ class MangaDetailsPresenter(
val source: Source, val source: Source,
val preferences: PreferencesHelper = Injekt.get(), val preferences: PreferencesHelper = Injekt.get(),
val coverCache: CoverCache = 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 downloadManager: DownloadManager = Injekt.get(),
private val chapterFilter: ChapterFilter = Injekt.get() private val chapterFilter: ChapterFilter = Injekt.get()
) : DownloadQueue.DownloadListener, LibraryServiceListener { ) : DownloadQueue.DownloadListener, LibraryServiceListener {
@ -334,7 +333,6 @@ class MangaDetailsPresenter(
} }
val networkManga = nManga.await() val networkManga = nManga.await()
val mangaWasInitalized = manga.initialized
if (networkManga != null) { if (networkManga != null) {
manga.copyFrom(networkManga) manga.copyFrom(networkManga)
manga.initialized = true manga.initialized = true
@ -405,7 +403,7 @@ class MangaDetailsPresenter(
} catch (e: Exception) { } catch (e: Exception) {
withContext(Dispatchers.Main) { controller.showError(trimException(e)) } withContext(Dispatchers.Main) { controller.showError(trimException(e)) }
return@launch return@launch
} ?: listOf() }
isLoading = false isLoading = false
try { try {
syncChaptersWithSource(db, chapters, manga, source) syncChaptersWithSource(db, chapters, manga, source)
@ -551,38 +549,6 @@ class MangaDetailsPresenter(
return db.getCategories().executeAsBlocking() 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<Category>) {
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<Int> {
val categories = db.getCategoriesForManga(manga).executeAsBlocking()
return categories.mapNotNull { it.id }.toTypedArray()
}
fun confirmDeletion() { fun confirmDeletion() {
coverCache.deleteFromCache(manga) coverCache.deleteFromCache(manga)
db.resetMangaInfo(manga).executeAsBlocking() db.resetMangaInfo(manga).executeAsBlocking()

View File

@ -11,13 +11,11 @@ import androidx.appcompat.widget.SearchView
import androidx.recyclerview.widget.DividerItemDecoration import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.snackbar.BaseTransientBottomBar
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import com.jakewharton.rxbinding.support.v7.widget.queryTextChangeEvents import com.jakewharton.rxbinding.support.v7.widget.queryTextChangeEvents
import eu.davidea.flexibleadapter.FlexibleAdapter import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.IFlexible import eu.davidea.flexibleadapter.items.IFlexible
import eu.kanade.tachiyomi.R 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.database.models.Manga
import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.preference.getOrDefault 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.model.FilterList
import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.ui.base.controller.NucleusController 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.main.MainActivity
import eu.kanade.tachiyomi.ui.manga.MangaDetailsController import eu.kanade.tachiyomi.ui.manga.MangaDetailsController
import eu.kanade.tachiyomi.ui.source.BrowseController import eu.kanade.tachiyomi.ui.source.BrowseController
import eu.kanade.tachiyomi.ui.webview.WebViewActivity 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.connectivityManager
import eu.kanade.tachiyomi.util.system.dpToPx import eu.kanade.tachiyomi.util.system.dpToPx
import eu.kanade.tachiyomi.util.system.openInBrowser import eu.kanade.tachiyomi.util.system.openInBrowser
@ -60,8 +58,7 @@ open class BrowseSourceController(bundle: Bundle) :
NucleusController<BrowseSourceControllerBinding, BrowseSourcePresenter>(bundle), NucleusController<BrowseSourceControllerBinding, BrowseSourcePresenter>(bundle),
FlexibleAdapter.OnItemClickListener, FlexibleAdapter.OnItemClickListener,
FlexibleAdapter.OnItemLongClickListener, FlexibleAdapter.OnItemLongClickListener,
FlexibleAdapter.EndlessScrollListener, FlexibleAdapter.EndlessScrollListener {
AddToLibraryCategoriesDialog.Listener {
constructor( constructor(
source: CatalogueSource, source: CatalogueSource,
@ -575,73 +572,23 @@ open class BrowseSourceController(bundle: Bundle) :
*/ */
override fun onItemLongClick(position: Int) { override fun onItemLongClick(position: Int) {
val manga = (adapter?.getItem(position) as? BrowseSourceItem?)?.manga ?: return val manga = (adapter?.getItem(position) as? BrowseSourceItem?)?.manga ?: return
val view = view ?: return
val activity = activity ?: return
snack?.dismiss() snack?.dismiss()
if (manga.favorite) { snack = manga.addOrRemoveToFavorites(
presenter.changeMangaFavorite(manga) presenter.db,
adapter?.notifyItemChanged(position) preferences,
snack = binding.sourceLayout.snack(R.string.removed_from_library, Snackbar.LENGTH_INDEFINITE) { view,
setAction(R.string.undo) { activity,
if (!manga.favorite) addManga(manga, position) onMangaAdded = {
} adapter?.notifyItemChanged(position)
addCallback( snack = view.snack(R.string.added_to_library)
object : BaseTransientBottomBar.BaseCallback<Snackbar>() { },
override fun onDismissed(transientBottomBar: Snackbar?, event: Int) { onMangaMoved = { adapter?.notifyItemChanged(position) },
super.onDismissed(transientBottomBar, event) onMangaDeleted = { presenter.confirmDeletion(manga) }
if (!manga.favorite) presenter.confirmDeletion(manga) )
} if (snack?.duration == Snackbar.LENGTH_INDEFINITE) {
}
)
}
(activity as? MainActivity)?.setUndoSnackBar(snack) (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<Category>) {
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)
} }
} }

View File

@ -7,7 +7,6 @@ import eu.kanade.tachiyomi.data.cache.CoverCache
import eu.kanade.tachiyomi.data.database.DatabaseHelper import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.data.database.models.Category import eu.kanade.tachiyomi.data.database.models.Category
import eu.kanade.tachiyomi.data.database.models.Manga 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.download.DownloadManager
import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.source.CatalogueSource import eu.kanade.tachiyomi.source.CatalogueSource
@ -37,7 +36,6 @@ import rx.subjects.PublishSubject
import timber.log.Timber import timber.log.Timber
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import java.util.Date
/** /**
* Presenter of [BrowseSourceController]. * Presenter of [BrowseSourceController].
@ -45,7 +43,7 @@ import java.util.Date
open class BrowseSourcePresenter( open class BrowseSourcePresenter(
sourceId: Long, sourceId: Long,
sourceManager: SourceManager = Injekt.get(), sourceManager: SourceManager = Injekt.get(),
private val db: DatabaseHelper = Injekt.get(), val db: DatabaseHelper = Injekt.get(),
private val prefs: PreferencesHelper = Injekt.get(), private val prefs: PreferencesHelper = Injekt.get(),
private val coverCache: CoverCache = Injekt.get() private val coverCache: CoverCache = Injekt.get()
) : BasePresenter<BrowseSourceController>() { ) : BasePresenter<BrowseSourceController>() {
@ -278,22 +276,6 @@ open class BrowseSourcePresenter(
.onErrorResumeNext { Observable.just(manga) } .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) { fun confirmDeletion(manga: Manga) {
coverCache.deleteFromCache(manga) coverCache.deleteFromCache(manga)
val downloadManager: DownloadManager = Injekt.get() val downloadManager: DownloadManager = Injekt.get()
@ -365,56 +347,4 @@ open class BrowseSourcePresenter(
fun getCategories(): List<Category> { fun getCategories(): List<Category> {
return db.getCategories().executeAsBlocking() 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<Int?> {
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<Category>) {
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<Category>) {
if (selectedCategories.isNotEmpty()) {
if (!manga.favorite) {
changeMangaFavorite(manga)
}
moveMangaToCategories(manga, selectedCategories.filter { it.id != 0 })
} else {
if (!manga.favorite) {
changeMangaFavorite(manga)
}
}
}
} }

View File

@ -1,9 +1,18 @@
package eu.kanade.tachiyomi.util 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.DatabaseHelper
import eu.kanade.tachiyomi.data.database.models.Manga 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.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.source.LocalSource 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 fun Manga.isLocal() = source == LocalSource.ID
@ -25,3 +34,108 @@ fun Manga.shouldDownloadNewChapters(db: DatabaseHelper, prefs: PreferencesHelper
return categoriesForManga.intersect(categoriesToDownload).isNotEmpty() 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<Snackbar>() {
override fun onDismissed(transientBottomBar: Snackbar?, event: Int) {
super.onDismissed(transientBottomBar, event)
if (!favorite) {
onMangaDeleted()
}
}
}
)
}
}
return null
}

View File

@ -0,0 +1,8 @@
<!-- drawable/plus.xml -->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:height="24dp"
android:width="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path android:fillColor="#000" android:pathData="M19,13H13V19H11V13H5V11H11V5H13V11H19V13Z" />
</vector>

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.checkbox.MaterialCheckBox xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/category_checkbox"
android:layout_width="match_parent"
android:layout_height="48sp"
android:layout_marginStart="12dp"
android:paddingStart="12dp"
android:paddingEnd="0dp"
android:textAppearance="@style/TextAppearance.MaterialComponents.Body2"
android:textSize="15sp"
tools:text="@string/category" />

View File

@ -50,7 +50,7 @@
android:layout_height="match_parent" android:layout_height="match_parent"
android:background="@null" android:background="@null"
android:imeOptions="actionDone" android:imeOptions="actionDone"
android:inputType="none" android:inputType="textCapSentences"
android:maxLines="1" android:maxLines="1"
android:singleLine="true" android:singleLine="true"
android:textColor="@color/textColorPrimary" android:textColor="@color/textColorPrimary"

View File

@ -2,17 +2,30 @@
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical"> android:orientation="vertical">
<EditText <com.google.android.material.textfield.TextInputLayout
android:id="@+id/title" android:id="@+id/category_text_layout"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:hint="@string/title"
android:layout_marginEnd="16dp"
android:layout_marginStart="16dp" android:layout_marginStart="16dp"
android:inputType="text" android:layout_marginEnd="16dp"
android:maxLines="1"/> android:hint="@string/title"
app:boxStrokeColor="@color/colorAccent"
app:endIconMode="clear_text"
app:hintEnabled="false"
app:hintTextColor="@color/colorAccent">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/category"
android:inputType="textCapSentences" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.checkbox.MaterialCheckBox <com.google.android.material.checkbox.MaterialCheckBox
android:id="@+id/download_new" android:id="@+id/download_new"

View File

@ -0,0 +1,131 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/source_filter_sheet"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/bottom_sheet_rounded_background"
app:layout_constraintVertical_chainStyle="packed"
android:backgroundTint="?android:attr/colorBackground">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/category_recycler_view"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:scrollbars="vertical"
app:layout_constraintTop_toBottomOf="@id/title_layout"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toTopOf="@id/button_layout"
tools:listitem="@layout/add_category_item"/>
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/title_layout"
android:layout_width="match_parent"
android:layout_height="?actionBarSize"
android:layout_gravity="top"
android:background="@drawable/bottom_sheet_rounded_background"
android:backgroundTint="?attr/colorSecondary"
android:elevation="0dp"
android:orientation="horizontal"
app:layout_constraintBottom_toTopOf="@id/category_recycler_view"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<com.google.android.material.textview.MaterialTextView
android:id="@+id/toolbar_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginStart="20dp"
android:ellipsize="end"
android:maxLines="1"
android:text="@string/add_x_to"
android:textAppearance="@style/TextAppearance.MaterialComponents.Headline6"
android:textColor="?actionBarTintColor"
android:textSize="17sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/new_category_button"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="Add manga to…"/>
<com.google.android.material.button.MaterialButton
android:id="@+id/new_category_button"
android:layout_width="wrap_content"
android:layout_height="0dp"
android:layout_marginEnd="16dp"
app:icon="@drawable/ic_plus_24dp"
app:iconTint="@color/colorAccent"
style="@style/Theme.Widget.Button.TextButton"
android:text="@string/new_category"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/toolbar_title"
app:layout_constraintTop_toTopOf="parent" />
<View
android:id="@+id/divider"
android:layout_width="match_parent"
android:layout_height="1dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
android:background="@color/divider"/>
</androidx.constraintlayout.widget.ConstraintLayout>
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/button_layout"
android:layout_width="match_parent"
android:layout_height="?actionBarSize"
android:layout_gravity="top"
android:background="@drawable/bottom_sheet_rounded_background"
android:backgroundTint="?attr/colorSecondary"
android:clickable="true"
android:elevation="0dp"
android:focusable="true"
android:orientation="horizontal"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/category_recycler_view">
<com.google.android.material.button.MaterialButton
android:id="@+id/cancel_button"
style="@style/Theme.Widget.Button.TextButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:text="@string/cancel"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/add_to_categories_button"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<com.google.android.material.button.MaterialButton
android:id="@+id/add_to_categories_button"
style="@style/Theme.Widget.Button.Primary"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="48dp"
android:layout_marginEnd="8dp"
android:text="@string/new_category"
app:iconTint="@color/colorAccent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/cancel_button"
app:layout_constraintTop_toTopOf="parent"
tools:text="Add to Default" />
<View
android:id="@+id/bottom_divider"
android:layout_width="match_parent"
android:layout_height="1dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
android:background="@color/divider"/>
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -21,6 +21,8 @@
<string name="author">Author</string> <string name="author">Author</string>
<string name="artist">Artist</string> <string name="artist">Artist</string>
<string name="description">Description</string> <string name="description">Description</string>
<string name="move_x_to">Move %1$s to…</string>
<string name="add_x_to">Add %1$s to…</string>
<!-- Status --> <!-- Status -->
<string name="ongoing">Ongoing</string> <string name="ongoing">Ongoing</string>
@ -69,6 +71,7 @@
<string name="adding_category_to_queue">Adding %1$s to update queue</string> <string name="adding_category_to_queue">Adding %1$s to update queue</string>
<string name="_already_in_queue">%1$s is already in queue</string> <string name="_already_in_queue">%1$s is already in queue</string>
<string name="create_new_category">Create new category</string> <string name="create_new_category">Create new category</string>
<string name="new_category">New category</string>
<string name="category_is_empty">Category is empty</string> <string name="category_is_empty">Category is empty</string>
<string name="category_is_hidden">Category is hidden</string> <string name="category_is_hidden">Category is hidden</string>
<string name="top_category">Top category (%1$s)</string> <string name="top_category">Top category (%1$s)</string>
@ -86,12 +89,12 @@
<string name="manage_category">Manage category</string> <string name="manage_category">Manage category</string>
<string name="rename_category">Rename category</string> <string name="rename_category">Rename category</string>
<string name="move_to_categories">Move to categories</string> <string name="move_to_categories">Move to categories</string>
<string name="add_to_categories">Choose which categories to add this to. If none are selected, this will be added to the "default" category</string>
<plurals name="category_plural"> <plurals name="category_plural">
<item quantity="one">%d category</item> <item quantity="one">%d category</item>
<item quantity="other">%d categories</item> <item quantity="other">%d categories</item>
</plurals> </plurals>
<string name="category_with_name_exists">A category with that name already exists!</string> <string name="category_with_name_exists">A category with that name already exists!</string>
<string name="category_cannot_be_blank">Category name cannot be blank</string>
<string name="category_deleted">Category deleted</string> <string name="category_deleted">Category deleted</string>
<string name="long_press_category">Press and hold to edit a category</string> <string name="long_press_category">Press and hold to edit a category</string>
<string name="jump_to_category">Jump to category</string> <string name="jump_to_category">Jump to category</string>
@ -788,6 +791,8 @@
<!-- Miscellaneous --> <!-- Miscellaneous -->
<string name="add">Add</string> <string name="add">Add</string>
<string name="add_to_">Add to %1$s</string>
<string name="added_to_">Added to %1$s</string>
<string name="all">All</string> <string name="all">All</string>
<string name="alphabetically">Alphabetically</string> <string name="alphabetically">Alphabetically</string>
<string name="always">Always</string> <string name="always">Always</string>
@ -801,6 +806,7 @@
<string name="bug_report">Report a Bug</string> <string name="bug_report">Report a Bug</string>
<string name="cancel">Cancel</string> <string name="cancel">Cancel</string>
<string name="center">Center</string> <string name="center">Center</string>
<string name="change">Change</string>
<string name="charging">Charging</string> <string name="charging">Charging</string>
<string name="clear">Clear</string> <string name="clear">Clear</string>
<string name="close">Close</string> <string name="close">Close</string>
@ -839,6 +845,7 @@
<string name="more">More</string> <string name="more">More</string>
<string name="move_to_bottom">Move to bottom</string> <string name="move_to_bottom">Move to bottom</string>
<string name="move_to_top">Move to top</string> <string name="move_to_top">Move to top</string>
<string name="move_to_">Move to %1$s</string>
<string name="moved_to_">Moved to %1$s</string> <string name="moved_to_">Moved to %1$s</string>
<string name="never">Never</string> <string name="never">Never</string>
<string name="newest">Newest</string> <string name="newest">Newest</string>