Selection and reodering favourites categories

This commit is contained in:
Koitharu
2022-07-09 16:52:55 +03:00
parent 451b9fc0f1
commit 80db7f0b74
30 changed files with 431 additions and 428 deletions

View File

@@ -23,7 +23,7 @@ class ListSelectionController(
private val activity: Activity,
private val decoration: AbstractSelectionItemDecoration,
private val registryOwner: SavedStateRegistryOwner,
private val callback: Callback,
private val callback: Callback2,
) : ActionMode.Callback, SavedStateRegistry.SavedStateProvider {
private var actionMode: ActionMode? = null
@@ -89,19 +89,19 @@ class ListSelectionController(
}
override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean {
return callback.onCreateActionMode(mode, menu)
return callback.onCreateActionMode(this, mode, menu)
}
override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean {
return callback.onPrepareActionMode(mode, menu)
return callback.onPrepareActionMode(this, mode, menu)
}
override fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean {
return callback.onActionItemClicked(mode, item)
return callback.onActionItemClicked(this, mode, item)
}
override fun onDestroyActionMode(mode: ActionMode) {
callback.onDestroyActionMode(mode)
callback.onDestroyActionMode(this, mode)
clear()
actionMode = null
}
@@ -114,7 +114,7 @@ class ListSelectionController(
private fun notifySelectionChanged() {
val count = decoration.checkedItemsCount
callback.onSelectionChanged(count)
callback.onSelectionChanged(this, count)
if (count == 0) {
actionMode?.finish()
} else {
@@ -131,17 +131,53 @@ class ListSelectionController(
notifySelectionChanged()
}
interface Callback : ActionMode.Callback {
@Deprecated("")
interface Callback : Callback2 {
fun onSelectionChanged(count: Int)
override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean
fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean
override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean
fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean
override fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean
fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean
override fun onDestroyActionMode(mode: ActionMode) = Unit
fun onDestroyActionMode(mode: ActionMode) = Unit
override fun onSelectionChanged(controller: ListSelectionController, count: Int) {
onSelectionChanged(count)
}
override fun onCreateActionMode(controller: ListSelectionController, mode: ActionMode, menu: Menu): Boolean {
return onCreateActionMode(mode, menu)
}
override fun onPrepareActionMode(controller: ListSelectionController, mode: ActionMode, menu: Menu): Boolean {
return onPrepareActionMode(mode, menu)
}
override fun onActionItemClicked(
controller: ListSelectionController,
mode: ActionMode,
item: MenuItem
): Boolean = onActionItemClicked(mode, item)
override fun onDestroyActionMode(controller: ListSelectionController, mode: ActionMode) {
onDestroyActionMode(mode)
}
}
interface Callback2 {
fun onSelectionChanged(controller: ListSelectionController, count: Int)
fun onCreateActionMode(controller: ListSelectionController, mode: ActionMode, menu: Menu): Boolean
fun onPrepareActionMode(controller: ListSelectionController, mode: ActionMode, menu: Menu): Boolean
fun onActionItemClicked(controller: ListSelectionController, mode: ActionMode, item: MenuItem): Boolean
fun onDestroyActionMode(controller: ListSelectionController, mode: ActionMode) = Unit
}
private inner class StateEventObserver : LifecycleEventObserver {

View File

@@ -11,11 +11,11 @@ import android.view.MenuItem
import androidx.core.graphics.Insets
import androidx.core.view.isVisible
import androidx.core.view.updatePadding
import com.google.android.material.R as materialR
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.BaseActivity
import org.koitharu.kotatsu.core.network.UserAgentInterceptor
import org.koitharu.kotatsu.databinding.ActivityBrowserBinding
import com.google.android.material.R as materialR
@SuppressLint("SetJavaScriptEnabled")
class BrowserActivity : BaseActivity<ActivityBrowserBinding>(), BrowserCallback {
@@ -59,8 +59,9 @@ class BrowserActivity : BaseActivity<ActivityBrowserBinding>(), BrowserCallback
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
super.onCreateOptionsMenu(menu)
menuInflater.inflate(R.menu.opt_browser, menu)
return super.onCreateOptionsMenu(menu)
return true
}
override fun onOptionsItemSelected(item: MenuItem): Boolean = when (item.itemId) {

View File

@@ -161,8 +161,9 @@ class DetailsActivity :
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
super.onCreateOptionsMenu(menu)
menuInflater.inflate(R.menu.opt_details, menu)
return super.onCreateOptionsMenu(menu)
return true
}
override fun onPrepareOptionsMenu(menu: Menu): Boolean {

View File

@@ -5,7 +5,10 @@ import kotlinx.coroutines.flow.*
import org.koitharu.kotatsu.core.db.MangaDatabase
import org.koitharu.kotatsu.core.db.entity.*
import org.koitharu.kotatsu.core.model.FavouriteCategory
import org.koitharu.kotatsu.favourites.data.*
import org.koitharu.kotatsu.favourites.data.FavouriteCategoryEntity
import org.koitharu.kotatsu.favourites.data.FavouriteEntity
import org.koitharu.kotatsu.favourites.data.FavouriteManga
import org.koitharu.kotatsu.favourites.data.toFavouriteCategory
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.SortOrder
import org.koitharu.kotatsu.tracker.work.TrackerNotificationChannels
@@ -122,6 +125,14 @@ class FavouritesRepository(
channels.deleteChannel(id)
}
suspend fun removeCategories(ids: Collection<Long>) {
db.withTransaction {
for (id in ids) {
removeCategory(id)
}
}
}
suspend fun setCategoryOrder(id: Long, order: SortOrder) {
db.favouriteCategoriesDao.updateOrder(id, order.name)
}

View File

@@ -1,178 +0,0 @@
package org.koitharu.kotatsu.favourites.ui
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.view.ActionMode
import androidx.appcompat.widget.PopupMenu
import androidx.core.graphics.Insets
import androidx.core.view.children
import androidx.core.view.isVisible
import androidx.core.view.updatePadding
import com.google.android.material.snackbar.Snackbar
import com.google.android.material.tabs.TabLayout
import com.google.android.material.tabs.TabLayoutMediator
import org.koin.androidx.viewmodel.ext.android.viewModel
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.BaseFragment
import org.koitharu.kotatsu.base.ui.util.ActionModeListener
import org.koitharu.kotatsu.core.model.FavouriteCategory
import org.koitharu.kotatsu.databinding.FragmentFavouritesBinding
import org.koitharu.kotatsu.databinding.ItemEmptyStateBinding
import org.koitharu.kotatsu.favourites.ui.categories.CategoriesEditDelegate
import org.koitharu.kotatsu.favourites.ui.categories.FavouritesCategoriesViewModel
import org.koitharu.kotatsu.favourites.ui.categories.adapter.CategoryListModel
import org.koitharu.kotatsu.favourites.ui.categories.edit.FavouritesCategoryEditActivity
import org.koitharu.kotatsu.utils.ext.addMenuProvider
import org.koitharu.kotatsu.utils.ext.getDisplayMessage
class FavouritesContainerFragment :
BaseFragment<FragmentFavouritesBinding>(),
FavouritesTabLongClickListener,
CategoriesEditDelegate.CategoriesEditCallback,
ActionModeListener,
View.OnClickListener {
private val viewModel by viewModel<FavouritesCategoriesViewModel>()
private val editDelegate by lazy(LazyThreadSafetyMode.NONE) {
CategoriesEditDelegate(requireContext(), this)
}
private var pagerAdapter: FavouritesPagerAdapter? = null
private var stubBinding: ItemEmptyStateBinding? = null
override fun onInflateView(
inflater: LayoutInflater,
container: ViewGroup?
) = FragmentFavouritesBinding.inflate(inflater, container, false)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val adapter = FavouritesPagerAdapter(this, this)
viewModel.allCategories.value?.let(::onCategoriesChanged)
binding.pager.adapter = adapter
pagerAdapter = adapter
TabLayoutMediator(binding.tabs, binding.pager, adapter).attach()
actionModeDelegate.addListener(this, viewLifecycleOwner)
addMenuProvider(FavouritesContainerMenuProvider(view.context))
viewModel.allCategories.observe(viewLifecycleOwner, ::onCategoriesChanged)
viewModel.onError.observe(viewLifecycleOwner, ::onError)
}
override fun onDestroyView() {
pagerAdapter = null
stubBinding = null
super.onDestroyView()
}
override fun onActionModeStarted(mode: ActionMode) {
binding.pager.isUserInputEnabled = false
binding.tabs.setTabsEnabled(false)
}
override fun onActionModeFinished(mode: ActionMode) {
binding.pager.isUserInputEnabled = true
binding.tabs.setTabsEnabled(true)
}
override fun onWindowInsetsChanged(insets: Insets) {
binding.tabs.apply {
updatePadding(
left = insets.left,
right = insets.right
)
}
}
private fun onCategoriesChanged(categories: List<CategoryListModel>) {
pagerAdapter?.replaceData(categories)
if (categories.isEmpty()) {
binding.pager.isVisible = false
binding.tabs.isVisible = false
showStub()
} else {
binding.pager.isVisible = true
binding.tabs.isVisible = true
(stubBinding?.root ?: binding.stubEmptyState).isVisible = false
}
}
private fun onError(e: Throwable) {
Snackbar.make(binding.pager, e.getDisplayMessage(resources), Snackbar.LENGTH_LONG).show()
}
override fun onTabLongClick(tabView: View, item: CategoryListModel): Boolean {
/*when (item) {
is CategoryListModel.All -> showAllCategoriesMenu(tabView)
is CategoryListModel.CategoryItem -> showCategoryMenu(tabView, item.category)
}*/
return true
}
override fun onClick(v: View) {
when (v.id) {
R.id.button_retry -> startActivity(FavouritesCategoryEditActivity.newIntent(v.context))
}
}
override fun onDeleteCategory(category: FavouriteCategory) {
viewModel.deleteCategory(category.id)
}
private fun TabLayout.setTabsEnabled(enabled: Boolean) {
val tabStrip = getChildAt(0) as? ViewGroup ?: return
for (tab in tabStrip.children) {
tab.isEnabled = enabled
}
}
private fun showCategoryMenu(tabView: View, category: FavouriteCategory) {
val menu = PopupMenu(tabView.context, tabView)
menu.inflate(R.menu.popup_category)
menu.setOnMenuItemClickListener {
when (it.itemId) {
R.id.action_remove -> editDelegate.deleteCategory(category)
R.id.action_edit -> startActivity(
FavouritesCategoryEditActivity.newIntent(
tabView.context,
category.id
)
)
else -> return@setOnMenuItemClickListener false
}
true
}
menu.show()
}
private fun showAllCategoriesMenu(tabView: View) {
val menu = PopupMenu(tabView.context, tabView)
menu.inflate(R.menu.popup_category_all)
menu.setOnMenuItemClickListener {
when (it.itemId) {
R.id.action_create -> startActivity(FavouritesCategoryEditActivity.newIntent(requireContext()))
R.id.action_hide -> viewModel.setAllCategoriesVisible(false)
}
true
}
menu.show()
}
private fun showStub() {
val stub = stubBinding ?: ItemEmptyStateBinding.bind(binding.stubEmptyState.inflate())
stub.root.isVisible = true
stub.icon.setImageResource(R.drawable.ic_heart_outline)
stub.textPrimary.setText(R.string.text_empty_holder_primary)
stub.textSecondary.setText(R.string.empty_favourite_categories)
stub.buttonRetry.setText(R.string.add)
stub.buttonRetry.isVisible = true
stub.buttonRetry.setOnClickListener(this)
stubBinding = stub
}
companion object {
fun newInstance() = FavouritesContainerFragment()
}
}

View File

@@ -1,28 +0,0 @@
package org.koitharu.kotatsu.favourites.ui
import android.content.Context
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import androidx.core.view.MenuProvider
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.favourites.ui.categories.FavouriteCategoriesActivity
class FavouritesContainerMenuProvider(
private val context: Context,
) : MenuProvider {
override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
menuInflater.inflate(R.menu.opt_favourites, menu)
}
override fun onMenuItemSelected(menuItem: MenuItem): Boolean {
return when (menuItem.itemId) {
R.id.action_categories -> {
context.startActivity(FavouriteCategoriesActivity.newIntent(context))
true
}
else -> false
}
}
}

View File

@@ -1,70 +0,0 @@
package org.koitharu.kotatsu.favourites.ui
import android.annotation.SuppressLint
import android.view.View
import androidx.fragment.app.Fragment
import androidx.recyclerview.widget.AsyncListDiffer
import androidx.recyclerview.widget.DiffUtil
import androidx.viewpager2.adapter.FragmentStateAdapter
import com.google.android.material.tabs.TabLayout
import com.google.android.material.tabs.TabLayoutMediator
import org.koitharu.kotatsu.favourites.ui.categories.adapter.CategoryListModel
import org.koitharu.kotatsu.favourites.ui.list.FavouritesListFragment
class FavouritesPagerAdapter(
fragment: Fragment,
private val longClickListener: FavouritesTabLongClickListener
) : FragmentStateAdapter(fragment.childFragmentManager, fragment.viewLifecycleOwner.lifecycle),
TabLayoutMediator.TabConfigurationStrategy,
View.OnLongClickListener {
private val differ = AsyncListDiffer(this, DiffCallback())
override fun getItemCount() = differ.currentList.size
override fun createFragment(position: Int): Fragment {
val item = differ.currentList[position]
return FavouritesListFragment.newInstance(item.category.id)
}
override fun getItemId(position: Int): Long {
return differ.currentList[position].category.id
}
override fun containsItem(itemId: Long): Boolean {
return differ.currentList.any { it.category.id == itemId }
}
override fun onConfigureTab(tab: TabLayout.Tab, position: Int) {
val item = differ.currentList[position]
tab.text = item.category.title
tab.view.tag = item.category.id
tab.view.setOnLongClickListener(this)
}
fun replaceData(data: List<CategoryListModel>) {
differ.submitList(data)
}
override fun onLongClick(v: View): Boolean {
val itemId = v.tag as? Long ?: return false
val item = differ.currentList.find { x -> x.category.id == itemId } ?: return false
return longClickListener.onTabLongClick(v, item)
}
private class DiffCallback : DiffUtil.ItemCallback<CategoryListModel>() {
override fun areItemsTheSame(
oldItem: CategoryListModel,
newItem: CategoryListModel
): Boolean {
return oldItem.category.id == newItem.category.id
}
@SuppressLint("DiffUtilEquals")
override fun areContentsTheSame(
oldItem: CategoryListModel,
newItem: CategoryListModel
): Boolean = oldItem == newItem
}
}

View File

@@ -1,9 +0,0 @@
package org.koitharu.kotatsu.favourites.ui
import android.view.View
import org.koitharu.kotatsu.favourites.ui.categories.adapter.CategoryListModel
fun interface FavouritesTabLongClickListener {
fun onTabLongClick(tabView: View, item: CategoryListModel): Boolean
}

View File

@@ -4,8 +4,6 @@ import androidx.lifecycle.LifecycleOwner
import androidx.recyclerview.widget.DiffUtil
import coil.ImageLoader
import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.model.FavouriteCategory
import org.koitharu.kotatsu.favourites.ui.categories.adapter.CategoryListModel
import org.koitharu.kotatsu.favourites.ui.categories.adapter.categoryAD
import org.koitharu.kotatsu.list.ui.adapter.ListStateHolderListener
@@ -17,7 +15,7 @@ import kotlin.jvm.internal.Intrinsics
class CategoriesAdapter(
coil: ImageLoader,
lifecycleOwner: LifecycleOwner,
onItemClickListener: OnListItemClickListener<FavouriteCategory>,
onItemClickListener: FavouriteCategoriesListListener,
listListener: ListStateHolderListener,
) : AsyncListDifferDelegationAdapter<ListModel>(DiffCallback()) {
@@ -43,7 +41,16 @@ class CategoriesAdapter(
}
override fun getChangePayload(oldItem: ListModel, newItem: ListModel): Any? {
return super.getChangePayload(oldItem, newItem)
return when {
oldItem is CategoryListModel && newItem is CategoryListModel -> {
if (oldItem.isReorderMode != newItem.isReorderMode) {
Unit
} else {
super.getChangePayload(oldItem, newItem)
}
}
else -> super.getChangePayload(oldItem, newItem)
}
}
}
}

View File

@@ -1,30 +0,0 @@
package org.koitharu.kotatsu.favourites.ui.categories
import android.content.Context
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.model.FavouriteCategory
import com.google.android.material.R as materialR
class CategoriesEditDelegate(
private val context: Context,
private val callback: CategoriesEditCallback
) {
fun deleteCategory(category: FavouriteCategory) {
MaterialAlertDialogBuilder(context, materialR.style.ThemeOverlay_Material3_MaterialAlertDialog_Centered)
.setMessage(context.getString(R.string.category_delete_confirm, category.title))
.setTitle(R.string.remove_category)
.setIcon(R.drawable.ic_delete)
.setNegativeButton(android.R.string.cancel, null)
.setPositiveButton(R.string.remove) { _, _ ->
callback.onDeleteCategory(category)
}.create()
.show()
}
interface CategoriesEditCallback {
fun onDeleteCategory(category: FavouriteCategory)
}
}

View File

@@ -0,0 +1,64 @@
package org.koitharu.kotatsu.favourites.ui.categories
import android.view.Menu
import android.view.MenuItem
import androidx.appcompat.view.ActionMode
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.list.ListSelectionController
import org.koitharu.kotatsu.favourites.ui.categories.edit.FavouritesCategoryEditActivity
import com.google.android.material.R as materialR
class CategoriesSelectionCallback(
private val recyclerView: RecyclerView,
private val viewModel: FavouritesCategoriesViewModel,
) : ListSelectionController.Callback2 {
override fun onSelectionChanged(controller: ListSelectionController, count: Int) {
recyclerView.invalidateItemDecorations()
}
override fun onCreateActionMode(controller: ListSelectionController, mode: ActionMode, menu: Menu): Boolean {
mode.menuInflater.inflate(R.menu.mode_category, menu)
return true
}
override fun onPrepareActionMode(controller: ListSelectionController, mode: ActionMode, menu: Menu): Boolean {
val isOneItem = controller.count == 1
menu.findItem(R.id.action_edit)?.isVisible = isOneItem
mode.title = controller.count.toString()
return true
}
override fun onActionItemClicked(controller: ListSelectionController, mode: ActionMode, item: MenuItem): Boolean {
return when (item.itemId) {
R.id.action_edit -> {
val id = controller.peekCheckedIds().singleOrNull() ?: return false
val context = recyclerView.context
val intent = FavouritesCategoryEditActivity.newIntent(context, id)
context.startActivity(intent)
mode.finish()
true
}
R.id.action_remove -> {
confirmDeleteCategories(controller.snapshot(), mode)
true
}
else -> false
}
}
private fun confirmDeleteCategories(ids: Set<Long>, mode: ActionMode) {
val context = recyclerView.context
MaterialAlertDialogBuilder(context, materialR.style.ThemeOverlay_Material3_MaterialAlertDialog_Centered)
.setMessage(R.string.categories_delete_confirm)
.setTitle(R.string.remove_category)
.setIcon(R.drawable.ic_delete)
.setNegativeButton(android.R.string.cancel, null)
.setPositiveButton(R.string.remove) { _, _ ->
viewModel.deleteCategories(ids)
mode.finish()
}.show()
}
}

View File

@@ -0,0 +1,57 @@
package org.koitharu.kotatsu.favourites.ui.categories
import android.content.Context
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.graphics.RectF
import android.view.View
import androidx.core.graphics.ColorUtils
import androidx.recyclerview.widget.RecyclerView
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.list.decor.AbstractSelectionItemDecoration
import org.koitharu.kotatsu.favourites.ui.categories.adapter.CategoryListModel
import org.koitharu.kotatsu.utils.ext.getItem
import org.koitharu.kotatsu.utils.ext.getThemeColor
import com.google.android.material.R as materialR
class CategoriesSelectionDecoration(context: Context) : AbstractSelectionItemDecoration() {
private val paint = Paint(Paint.ANTI_ALIAS_FLAG)
private val radius = context.resources.getDimension(R.dimen.list_selector_corner)
private val strokeColor = context.getThemeColor(materialR.attr.colorPrimary, Color.RED)
private val fillColor = ColorUtils.setAlphaComponent(
ColorUtils.blendARGB(strokeColor, context.getThemeColor(materialR.attr.colorSurface), 0.8f),
0x74
)
private val padding = context.resources.getDimension(R.dimen.grid_spacing_outer)
init {
paint.strokeWidth = context.resources.getDimension(R.dimen.selection_stroke_width)
hasForeground = true
hasBackground = false
isIncludeDecorAndMargins = false
}
override fun getItemId(parent: RecyclerView, child: View): Long {
val holder = parent.getChildViewHolder(child) ?: return RecyclerView.NO_ID
val item = holder.getItem(CategoryListModel::class.java) ?: return RecyclerView.NO_ID
return item.category.id
}
override fun onDrawForeground(
canvas: Canvas,
parent: RecyclerView,
child: View,
bounds: RectF,
state: RecyclerView.State,
) {
bounds.inset(padding, padding)
paint.color = fillColor
paint.style = Paint.Style.FILL
canvas.drawRoundRect(bounds, radius, radius, paint)
paint.color = strokeColor
paint.style = Paint.Style.STROKE
canvas.drawRoundRect(bounds, radius, radius, paint)
}
}

View File

@@ -4,9 +4,12 @@ import android.app.ActivityOptions
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.view.Menu
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import androidx.core.graphics.Insets
import androidx.core.view.isVisible
import androidx.core.view.updateLayoutParams
import androidx.core.view.updatePadding
import androidx.recyclerview.widget.ItemTouchHelper
@@ -16,7 +19,7 @@ import org.koin.android.ext.android.get
import org.koin.androidx.viewmodel.ext.android.viewModel
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.BaseActivity
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.base.ui.list.ListSelectionController
import org.koitharu.kotatsu.core.model.FavouriteCategory
import org.koitharu.kotatsu.databinding.ActivityCategoriesBinding
import org.koitharu.kotatsu.favourites.ui.FavouritesActivity
@@ -29,50 +32,78 @@ import org.koitharu.kotatsu.utils.ext.measureHeight
class FavouriteCategoriesActivity :
BaseActivity<ActivityCategoriesBinding>(),
OnListItemClickListener<FavouriteCategory>,
FavouriteCategoriesListListener,
View.OnClickListener,
CategoriesEditDelegate.CategoriesEditCallback,
ListStateHolderListener {
private val viewModel by viewModel<FavouritesCategoriesViewModel>()
private lateinit var adapter: CategoriesAdapter
private lateinit var reorderHelper: ItemTouchHelper
private lateinit var editDelegate: CategoriesEditDelegate
private lateinit var selectionController: ListSelectionController
private var reorderHelper: ItemTouchHelper? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(ActivityCategoriesBinding.inflate(layoutInflater))
supportActionBar?.setDisplayHomeAsUpEnabled(true)
adapter = CategoriesAdapter(get(), this, this, this)
editDelegate = CategoriesEditDelegate(this, this)
selectionController = ListSelectionController(
activity = this,
decoration = CategoriesSelectionDecoration(this),
registryOwner = this,
callback = CategoriesSelectionCallback(binding.recyclerView, viewModel),
)
binding.buttonDone.setOnClickListener(this)
selectionController.attachToRecyclerView(binding.recyclerView)
binding.recyclerView.setHasFixedSize(true)
binding.recyclerView.adapter = adapter
binding.fabAdd.setOnClickListener(this)
reorderHelper = ItemTouchHelper(ReorderHelperCallback())
reorderHelper.attachToRecyclerView(binding.recyclerView)
viewModel.detalizedCategories.observe(this, ::onCategoriesChanged)
viewModel.onError.observe(this, ::onError)
viewModel.isInReorderMode.observe(this, ::onReorderModeChanged)
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
super.onCreateOptionsMenu(menu)
menuInflater.inflate(R.menu.opt_categories, menu)
return true
}
override fun onPrepareOptionsMenu(menu: Menu): Boolean {
menu.findItem(R.id.action_reorder)?.isVisible = !viewModel.isInReorderMode()
return super.onPrepareOptionsMenu(menu)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) {
R.id.action_reorder -> {
viewModel.setReorderMode(true)
true
}
else -> super.onOptionsItemSelected(item)
}
}
override fun onBackPressed() {
if (viewModel.isInReorderMode()) {
viewModel.setReorderMode(false)
} else {
super.onBackPressed()
}
}
override fun onClick(v: View) {
when (v.id) {
R.id.button_done -> viewModel.setReorderMode(false)
R.id.fab_add -> startActivity(FavouritesCategoryEditActivity.newIntent(this))
}
}
override fun onItemClick(item: FavouriteCategory, view: View) {
/*val menu = PopupMenu(view.context, view)
menu.inflate(R.menu.popup_category)
menu.setOnMenuItemClickListener { menuItem ->
when (menuItem.itemId) {
R.id.action_remove -> editDelegate.deleteCategory(item)
R.id.action_edit -> startActivity(FavouritesCategoryEditActivity.newIntent(this, item.id))
}
true
if (viewModel.isInReorderMode() || selectionController.onItemClick(item.id)) {
return
}
menu.show()*/
val intent = FavouritesActivity.newIntent(this, item)
val options =
ActivityOptions.makeScaleUpAnimation(view, view.width / 2, view.height / 2, view.width, view.height)
@@ -80,9 +111,11 @@ class FavouriteCategoriesActivity :
}
override fun onItemLongClick(item: FavouriteCategory, view: View): Boolean {
val viewHolder = binding.recyclerView.findContainingViewHolder(view) ?: return false
reorderHelper.startDrag(viewHolder)
return true
return !viewModel.isInReorderMode() && selectionController.onItemLongClick(item.id)
}
override fun onDragHandleTouch(holder: RecyclerView.ViewHolder): Boolean {
return reorderHelper?.startDrag(holder) != null
}
override fun onRetryClick(error: Throwable) = Unit
@@ -111,8 +144,21 @@ class FavouriteCategoriesActivity :
.show()
}
override fun onDeleteCategory(category: FavouriteCategory) {
viewModel.deleteCategory(category.id)
private fun onReorderModeChanged(isReorderMode: Boolean) {
reorderHelper?.attachToRecyclerView(null)
reorderHelper = if (isReorderMode) {
selectionController.clear()
binding.fabAdd.hide()
ItemTouchHelper(ReorderHelperCallback()).apply {
attachToRecyclerView(binding.recyclerView)
}
} else {
binding.fabAdd.show()
null
}
binding.recyclerView.isNestedScrollingEnabled = !isReorderMode
invalidateOptionsMenu()
binding.buttonDone.isVisible = isReorderMode
}
private inner class ReorderHelperCallback : ItemTouchHelper.SimpleCallback(

View File

@@ -0,0 +1,10 @@
package org.koitharu.kotatsu.favourites.ui.categories
import androidx.recyclerview.widget.RecyclerView
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.model.FavouriteCategory
interface FavouriteCategoriesListListener : OnListItemClickListener<FavouriteCategory> {
fun onDragHandleTouch(holder: RecyclerView.ViewHolder): Boolean
}

View File

@@ -1,9 +1,11 @@
package org.koitharu.kotatsu.favourites.ui.categories
import androidx.lifecycle.asLiveData
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.combine
import org.koitharu.kotatsu.base.ui.BaseViewModel
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.favourites.domain.FavouritesRepository
@@ -20,6 +22,9 @@ class FavouritesCategoriesViewModel(
) : BaseViewModel() {
private var reorderJob: Job? = null
private val isReorder = MutableStateFlow(false)
val isInReorderMode = isReorder.asLiveData(viewModelScope.coroutineContext)
val allCategories = repository.observeCategories()
.mapItems {
@@ -27,19 +32,23 @@ class FavouritesCategoriesViewModel(
mangaCount = 0,
covers = listOf(),
category = it,
isReorderMode = false,
)
}.asLiveDataDistinct(viewModelScope.coroutineContext + Dispatchers.Default, emptyList())
val detalizedCategories = repository.observeCategoriesWithDetails()
.map {
it.map { (category, covers) ->
CategoryListModel(
mangaCount = covers.size,
covers = covers.take(3),
category = category,
)
}
}.asLiveDataDistinct(viewModelScope.coroutineContext + Dispatchers.Default, listOf(LoadingState))
val detalizedCategories = combine(
repository.observeCategoriesWithDetails(),
isReorder,
) { list, reordering ->
list.map { (category, covers) ->
CategoryListModel(
mangaCount = covers.size,
covers = covers.take(3),
category = category,
isReorderMode = reordering,
)
}
}.asLiveDataDistinct(viewModelScope.coroutineContext + Dispatchers.Default, listOf(LoadingState))
fun deleteCategory(id: Long) {
launchJob {
@@ -47,10 +56,22 @@ class FavouritesCategoriesViewModel(
}
}
fun deleteCategories(ids: Set<Long>) {
launchJob {
repository.removeCategories(ids)
}
}
fun setAllCategoriesVisible(isVisible: Boolean) {
settings.isAllFavouritesVisible = isVisible
}
fun isInReorderMode(): Boolean = isReorder.value
fun setReorderMode(isReorderMode: Boolean) {
isReorder.value = isReorderMode
}
fun reorderCategories(oldPos: Int, newPos: Int) {
val prevJob = reorderJob
reorderJob = launchJob(Dispatchers.Default) {

View File

@@ -1,8 +1,14 @@
package org.koitharu.kotatsu.favourites.ui.categories.adapter
import android.content.res.ColorStateList
import android.graphics.Color
import android.graphics.drawable.ColorDrawable
import android.view.MotionEvent
import android.view.View
import android.view.View.OnClickListener
import android.view.View.OnLongClickListener
import android.view.View.*
import androidx.core.graphics.ColorUtils
import androidx.core.view.isVisible
import androidx.core.widget.ImageViewCompat
import androidx.lifecycle.LifecycleOwner
import coil.ImageLoader
import coil.request.Disposable
@@ -10,32 +16,50 @@ import coil.size.Scale
import coil.util.CoilUtils
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.model.FavouriteCategory
import org.koitharu.kotatsu.databinding.ItemCategoryBinding
import org.koitharu.kotatsu.favourites.ui.categories.FavouriteCategoriesListListener
import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.utils.ext.animatorDurationScale
import org.koitharu.kotatsu.utils.ext.enqueueWith
import org.koitharu.kotatsu.utils.ext.getThemeColor
import org.koitharu.kotatsu.utils.ext.newImageRequest
fun categoryAD(
coil: ImageLoader,
lifecycleOwner: LifecycleOwner,
clickListener: OnListItemClickListener<FavouriteCategory>
clickListener: FavouriteCategoriesListListener,
) = adapterDelegateViewBinding<CategoryListModel, ListModel, ItemCategoryBinding>(
{ inflater, parent -> ItemCategoryBinding.inflate(inflater, parent, false) }
) {
val eventListener = object : OnClickListener, OnLongClickListener {
val eventListener = object : OnClickListener, OnLongClickListener, OnTouchListener {
override fun onClick(v: View) = clickListener.onItemClick(item.category, v)
override fun onLongClick(v: View) = clickListener.onItemLongClick(item.category, v)
override fun onTouch(v: View?, event: MotionEvent): Boolean = item.isReorderMode &&
event.actionMasked == MotionEvent.ACTION_DOWN &&
clickListener.onDragHandleTouch(this@adapterDelegateViewBinding)
}
val backgroundColor = context.getThemeColor(android.R.attr.colorBackground)
ImageViewCompat.setImageTintList(
binding.imageViewCover3,
ColorStateList.valueOf(ColorUtils.setAlphaComponent(backgroundColor, 153))
)
ImageViewCompat.setImageTintList(
binding.imageViewCover2,
ColorStateList.valueOf(ColorUtils.setAlphaComponent(backgroundColor, 76))
)
val fallback = ColorDrawable(Color.TRANSPARENT)
val coverViews = arrayOf(binding.imageViewCover1, binding.imageViewCover2, binding.imageViewCover3)
val imageRequests = arrayOfNulls<Disposable?>(coverViews.size)
val crossFadeDuration = (context.resources.getInteger(R.integer.config_defaultAnimTime) *
context.animatorDurationScale).toInt()
itemView.setOnClickListener(eventListener)
itemView.setOnLongClickListener(eventListener)
itemView.setOnTouchListener(eventListener)
bind {
imageRequests.forEach { it?.dispose() }
binding.imageViewHandle.isVisible = item.isReorderMode
binding.textViewTitle.text = item.category.title
binding.textViewSubtitle.text = context.resources.getQuantityString(
R.plurals.items,
@@ -45,7 +69,8 @@ fun categoryAD(
repeat(coverViews.size) { i ->
imageRequests[i] = coverViews[i].newImageRequest(item.covers.getOrNull(i))
.placeholder(R.drawable.ic_placeholder)
.fallback(null)
.crossfade(crossFadeDuration * (i + 1))
.fallback(fallback)
.error(R.drawable.ic_placeholder)
.scale(Scale.FILL)
.allowRgb565(true)

View File

@@ -7,6 +7,7 @@ class CategoryListModel(
val mangaCount: Int,
val covers: List<String>,
val category: FavouriteCategory,
val isReorderMode: Boolean,
) : ListModel {
override fun equals(other: Any?): Boolean {
@@ -16,6 +17,7 @@ class CategoryListModel(
other as CategoryListModel
if (mangaCount != other.mangaCount) return false
if (isReorderMode != other.isReorderMode) return false
if (covers != other.covers) return false
if (category.id != other.category.id) return false
if (category.title != other.category.title) return false
@@ -26,6 +28,7 @@ class CategoryListModel(
override fun hashCode(): Int {
var result = mangaCount
result = 31 * result + isReorderMode.hashCode()
result = 31 * result + covers.hashCode()
result = 31 * result + category.id.hashCode()
result = 31 * result + category.title.hashCode()

View File

@@ -11,10 +11,8 @@ import org.koin.core.parameter.parametersOf
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.BaseBottomSheet
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.model.FavouriteCategory
import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga
import org.koitharu.kotatsu.databinding.DialogFavoriteCategoriesBinding
import org.koitharu.kotatsu.favourites.ui.categories.CategoriesEditDelegate
import org.koitharu.kotatsu.favourites.ui.categories.edit.FavouritesCategoryEditActivity
import org.koitharu.kotatsu.favourites.ui.categories.select.adapter.MangaCategoriesAdapter
import org.koitharu.kotatsu.favourites.ui.categories.select.model.MangaCategoryItem
@@ -25,7 +23,6 @@ import org.koitharu.kotatsu.utils.ext.withArgs
class FavouriteCategoriesBottomSheet :
BaseBottomSheet<DialogFavoriteCategoriesBinding>(),
OnListItemClickListener<MangaCategoryItem>,
CategoriesEditDelegate.CategoriesEditCallback,
View.OnClickListener {
private val viewModel by viewModel<MangaCategoriesViewModel> {
@@ -66,8 +63,6 @@ class FavouriteCategoriesBottomSheet :
viewModel.setChecked(item.id, !item.isChecked)
}
override fun onDeleteCategory(category: FavouriteCategory) = Unit
private fun onContentChanged(categories: List<MangaCategoryItem>) {
adapter?.items = categories
}

View File

@@ -8,7 +8,7 @@ import androidx.appcompat.content.res.AppCompatResources
import androidx.core.graphics.ColorUtils
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.history.domain.PROGRESS_NONE
import kotlin.math.roundToInt
import org.koitharu.kotatsu.utils.ext.scale
class ReadingProgressDrawable(
context: Context,
@@ -105,7 +105,7 @@ class ReadingProgressDrawable(
if (hasText) {
if (checkDrawable != null && progress >= 1f - Math.ulp(progress)) {
tempRect.set(bounds)
tempRect *= 0.6
tempRect.scale(0.6)
checkDrawable.bounds = tempRect
checkDrawable.draw(canvas)
} else {
@@ -139,13 +139,4 @@ class ReadingProgressDrawable(
paint.getTextBounds(text, 0, text.length, tempRect)
return testTextSize * width / tempRect.width()
}
private operator fun Rect.timesAssign(factor: Double) {
val newWidth = (width() * factor).roundToInt()
val newHeight = (height() * factor).roundToInt()
inset(
(width() - newWidth) / 2,
(height() - newHeight) / 2,
)
}
}

View File

@@ -133,8 +133,9 @@ class ReaderActivity :
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
super.onCreateOptionsMenu(menu)
menuInflater.inflate(R.menu.opt_reader_top, menu)
return super.onCreateOptionsMenu(menu)
return true
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {

View File

@@ -62,8 +62,9 @@ class SettingsActivity :
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
super.onCreateOptionsMenu(menu)
menuInflater.inflate(R.menu.opt_settings, menu)
return super.onCreateOptionsMenu(menu)
return true
}
override fun onOptionsItemSelected(item: MenuItem): Boolean = when (item.itemId) {

View File

@@ -0,0 +1,13 @@
package org.koitharu.kotatsu.utils.ext
import android.graphics.Rect
import kotlin.math.roundToInt
fun Rect.scale(factor: Double) {
val newWidth = (width() * factor).roundToInt()
val newHeight = (height() * factor).roundToInt()
inset(
(width() - newWidth) / 2,
(height() - newHeight) / 2,
)
}

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<vector
xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?attr/colorControlNormal"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#000"
android:pathData="M20.71,7.04C21.1,6.65 21.1,6 20.71,5.63L18.37,3.29C18,2.9 17.35,2.9 16.96,3.29L15.12,5.12L18.87,8.87M3,17.25V21H6.75L17.81,9.93L14.06,6.18L3,17.25Z" />
</vector>

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<vector
xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?colorControlNormal"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#000"
android:pathData="M9,3L5,7H8V14H10V7H13M16,17V10H14V17H11L15,21L19,17H16Z" />
</vector>

View File

@@ -6,9 +6,9 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/list_selector"
android:paddingVertical="12dp"
android:minHeight="98dp"
android:paddingStart="?android:listPreferredItemPaddingStart"
android:paddingEnd="?listPreferredItemPaddingEnd">
tools:ignore="RtlSymmetry">
<com.google.android.material.imageview.ShapeableImageView
android:id="@+id/imageView_cover3"
@@ -22,28 +22,30 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:shapeAppearanceOverlay="@style/ShapeAppearanceOverlay.Kotatsu.Cover.Small"
tools:src="@tools:sample/backgrounds/scenic" />
app:tintMode="src_atop"
tools:src="@tools:sample/backgrounds/scenic"
tools:tint="#99FFFFFF" />
<com.google.android.material.imageview.ShapeableImageView
android:id="@+id/imageView_cover2"
android:layout_width="0dp"
android:layout_height="64dp"
android:layout_marginStart="12dp"
android:alpha="50"
android:scaleType="centerCrop"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintDimensionRatio="W,13:18"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:shapeAppearanceOverlay="@style/ShapeAppearanceOverlay.Kotatsu.Cover.Small"
tools:src="@tools:sample/backgrounds/scenic" />
app:tintMode="src_atop"
tools:src="@tools:sample/backgrounds/scenic"
tools:tint="#4DFFFFFF" />
<com.google.android.material.imageview.ShapeableImageView
android:id="@+id/imageView_cover1"
android:layout_width="0dp"
android:layout_height="64dp"
android:layout_marginTop="12dp"
android:alpha="120"
android:scaleType="centerCrop"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintDimensionRatio="W,13:18"
@@ -57,6 +59,7 @@
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/margin_normal"
android:layout_marginEnd="?listPreferredItemPaddingEnd"
android:ellipsize="end"
android:singleLine="true"
android:textAppearance="?attr/textAppearanceBodyLarge"
@@ -73,6 +76,7 @@
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/margin_normal"
android:layout_marginTop="4dp"
android:layout_marginEnd="?listPreferredItemPaddingEnd"
android:singleLine="true"
android:textAppearance="?attr/textAppearanceBodySmall"
app:layout_constraintBottom_toBottomOf="parent"
@@ -82,4 +86,17 @@
app:layout_constraintVertical_chainStyle="packed"
tools:text="@tools:sample/lorem[1]" />
<ImageView
android:id="@+id/imageView_handle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="?selectableItemBackgroundBorderless"
android:contentDescription="@string/reorder"
android:padding="@dimen/margin_normal"
android:src="@drawable/ic_reorder_handle"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
tools:visibility="visible" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<menu
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/action_edit"
android:icon="@drawable/ic_edit"
android:title="@string/edit"
app:showAsAction="ifRoom" />
<item
android:id="@+id/action_remove"
android:icon="@drawable/ic_delete"
android:title="@string/remove"
app:showAsAction="ifRoom" />
</menu>

View File

@@ -4,9 +4,9 @@
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/action_categories"
android:orderInCategory="50"
android:title="@string/categories_"
app:showAsAction="never" />
android:id="@+id/action_reorder"
android:icon="@drawable/ic_reorder"
android:title="@string/reorder"
app:showAsAction="ifRoom" />
</menu>

View File

@@ -1,13 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<menu
xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/action_remove"
android:title="@string/remove" />
<item
android:id="@+id/action_edit"
android:title="@string/edit" />
</menu>

View File

@@ -1,13 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<menu
xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/action_hide"
android:title="@string/hide" />
<item
android:id="@+id/action_create"
android:title="@string/create_category" />
</menu>

View File

@@ -330,4 +330,6 @@
<string name="no_manga_sources">No manga sources</string>
<string name="no_manga_sources_text">Enable manga sources to read manga online</string>
<string name="random">Random</string>
<string name="categories_delete_confirm">Are you sure you want to delete the selected favorite categories?\nAll manga in it will be lost and this cannot be undone.</string>
<string name="reorder">Reorder</string>
</resources>