Favourites manage activity

pull/440/head
Koitharu 3 years ago
parent 5785a2d5d1
commit 45b2f2337a
Signed by: Koitharu
GPG Key ID: 676DEE768C17A9D7

@ -26,7 +26,7 @@ data class FavouriteCategory(
if (previousState !is FavouriteCategory) { if (previousState !is FavouriteCategory) {
return null return null
} }
return if (isTrackingEnabled != previousState.isTrackingEnabled || isVisibleInLibrary != isVisibleInLibrary) { return if (isTrackingEnabled != previousState.isTrackingEnabled || isVisibleInLibrary != previousState.isVisibleInLibrary) {
ListModelDiffCallback.PAYLOAD_CHECKED_CHANGED ListModelDiffCallback.PAYLOAD_CHECKED_CHANGED
} else { } else {
null null

@ -3,16 +3,10 @@ package org.koitharu.kotatsu.favourites.ui.categories
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.transition.Fade
import android.transition.TransitionManager
import android.view.Menu
import android.view.MenuItem
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.activity.OnBackPressedCallback
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.core.graphics.Insets import androidx.core.graphics.Insets
import androidx.core.view.isVisible
import androidx.core.view.updateLayoutParams import androidx.core.view.updateLayoutParams
import androidx.core.view.updatePadding import androidx.core.view.updatePadding
import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.ItemTouchHelper
@ -48,16 +42,14 @@ class FavouriteCategoriesActivity :
private val viewModel by viewModels<FavouritesCategoriesViewModel>() private val viewModel by viewModels<FavouritesCategoriesViewModel>()
private lateinit var exitReorderModeCallback: ExitReorderModeCallback
private lateinit var adapter: CategoriesAdapter private lateinit var adapter: CategoriesAdapter
private lateinit var selectionController: ListSelectionController private lateinit var selectionController: ListSelectionController
private var reorderHelper: ItemTouchHelper? = null private lateinit var reorderHelper: ItemTouchHelper
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(ActivityCategoriesBinding.inflate(layoutInflater)) setContentView(ActivityCategoriesBinding.inflate(layoutInflater))
supportActionBar?.setDisplayHomeAsUpEnabled(true) supportActionBar?.setDisplayHomeAsUpEnabled(true)
exitReorderModeCallback = ExitReorderModeCallback(viewModel)
adapter = CategoriesAdapter(coil, this, this, this) adapter = CategoriesAdapter(coil, this, this, this)
selectionController = ListSelectionController( selectionController = ListSelectionController(
activity = this, activity = this,
@ -65,49 +57,27 @@ class FavouriteCategoriesActivity :
registryOwner = this, registryOwner = this,
callback = CategoriesSelectionCallback(viewBinding.recyclerView, viewModel), callback = CategoriesSelectionCallback(viewBinding.recyclerView, viewModel),
) )
viewBinding.buttonDone.setOnClickListener(this)
selectionController.attachToRecyclerView(viewBinding.recyclerView) selectionController.attachToRecyclerView(viewBinding.recyclerView)
viewBinding.recyclerView.setHasFixedSize(true) viewBinding.recyclerView.setHasFixedSize(true)
viewBinding.recyclerView.adapter = adapter viewBinding.recyclerView.adapter = adapter
viewBinding.fabAdd.setOnClickListener(this) viewBinding.fabAdd.setOnClickListener(this)
onBackPressedDispatcher.addCallback(exitReorderModeCallback)
reorderHelper = ItemTouchHelper(ReorderHelperCallback()).apply {
attachToRecyclerView(viewBinding.recyclerView)
}
viewModel.detalizedCategories.observe(this, ::onCategoriesChanged) viewModel.detalizedCategories.observe(this, ::onCategoriesChanged)
viewModel.onError.observeEvent(this, SnackbarErrorObserver(viewBinding.recyclerView, null)) viewModel.onError.observeEvent(this, SnackbarErrorObserver(viewBinding.recyclerView, null))
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() && !viewModel.isEmpty()
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 onClick(v: View) { override fun onClick(v: View) {
when (v.id) { when (v.id) {
R.id.button_done -> viewModel.setReorderMode(false)
R.id.fab_add -> startActivity(FavouritesCategoryEditActivity.newIntent(this)) R.id.fab_add -> startActivity(FavouritesCategoryEditActivity.newIntent(this))
} }
} }
override fun onItemClick(item: FavouriteCategory, view: View) { override fun onItemClick(item: FavouriteCategory, view: View) {
if (viewModel.isInReorderMode() || selectionController.onItemClick(item.id)) { if (selectionController.onItemClick(item.id)) {
return return
} }
val intent = FavouritesActivity.newIntent(this, item) val intent = FavouritesActivity.newIntent(this, item)
@ -116,11 +86,12 @@ class FavouriteCategoriesActivity :
} }
override fun onItemLongClick(item: FavouriteCategory, view: View): Boolean { override fun onItemLongClick(item: FavouriteCategory, view: View): Boolean {
return !viewModel.isInReorderMode() && selectionController.onItemLongClick(item.id) return selectionController.onItemLongClick(item.id)
} }
override fun onDragHandleTouch(holder: RecyclerView.ViewHolder): Boolean { override fun onDragHandleTouch(holder: RecyclerView.ViewHolder): Boolean {
return reorderHelper?.startDrag(holder) != null reorderHelper.startDrag(holder)
return true
} }
override fun onRetryClick(error: Throwable) = Unit override fun onRetryClick(error: Throwable) = Unit
@ -147,28 +118,6 @@ class FavouriteCategoriesActivity :
invalidateOptionsMenu() invalidateOptionsMenu()
} }
private fun onReorderModeChanged(isReorderMode: Boolean) {
val transition = Fade().apply {
duration = resources.getInteger(android.R.integer.config_shortAnimTime).toLong()
}
TransitionManager.beginDelayedTransition(viewBinding.toolbar, transition)
reorderHelper?.attachToRecyclerView(null)
reorderHelper = if (isReorderMode) {
selectionController.clear()
viewBinding.fabAdd.hide()
ItemTouchHelper(ReorderHelperCallback()).apply {
attachToRecyclerView(viewBinding.recyclerView)
}
} else {
viewBinding.fabAdd.show()
null
}
viewBinding.recyclerView.isNestedScrollingEnabled = !isReorderMode
invalidateOptionsMenu()
viewBinding.buttonDone.isVisible = isReorderMode
exitReorderModeCallback.isEnabled = isReorderMode
}
private inner class ReorderHelperCallback : ItemTouchHelper.SimpleCallback( private inner class ReorderHelperCallback : ItemTouchHelper.SimpleCallback(
ItemTouchHelper.DOWN or ItemTouchHelper.UP, ItemTouchHelper.DOWN or ItemTouchHelper.UP,
0, 0,
@ -202,14 +151,10 @@ class FavouriteCategoriesActivity :
} }
override fun isLongPressDragEnabled(): Boolean = false override fun isLongPressDragEnabled(): Boolean = false
}
private class ExitReorderModeCallback(
private val viewModel: FavouritesCategoriesViewModel,
) : OnBackPressedCallback(viewModel.isInReorderMode()) {
override fun handleOnBackPressed() { override fun onSelectedChanged(viewHolder: RecyclerView.ViewHolder?, actionState: Int) {
viewModel.setReorderMode(false) super.onSelectedChanged(viewHolder, actionState)
viewBinding.recyclerView.isNestedScrollingEnabled = actionState == ItemTouchHelper.ACTION_STATE_IDLE
} }
} }

@ -4,9 +4,8 @@ import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.plus import kotlinx.coroutines.plus
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
@ -27,30 +26,26 @@ class FavouritesCategoriesViewModel @Inject constructor(
) : BaseViewModel() { ) : BaseViewModel() {
private var reorderJob: Job? = null private var reorderJob: Job? = null
val isInReorderMode = MutableStateFlow(false)
val detalizedCategories = combine( val detalizedCategories = repository.observeCategoriesWithCovers()
repository.observeCategoriesWithCovers(), .map { list ->
isInReorderMode, list.map { (category, covers) ->
) { list, reordering -> CategoryListModel(
list.map { (category, covers) -> mangaCount = covers.size,
CategoryListModel( covers = covers.take(3),
mangaCount = covers.size, category = category,
covers = covers.take(3), )
category = category, }.ifEmpty {
isReorderMode = reordering, listOf(
) EmptyState(
}.ifEmpty { icon = R.drawable.ic_empty_favourites,
listOf( textPrimary = R.string.text_empty_holder_primary,
EmptyState( textSecondary = R.string.empty_favourite_categories,
icon = R.drawable.ic_empty_favourites, actionStringRes = 0,
textPrimary = R.string.text_empty_holder_primary, ),
textSecondary = R.string.empty_favourite_categories, )
actionStringRes = 0, }
), }.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, listOf(LoadingState))
)
}
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, listOf(LoadingState))
fun deleteCategory(id: Long) { fun deleteCategory(id: Long) {
launchJob { launchJob {
@ -68,14 +63,8 @@ class FavouritesCategoriesViewModel @Inject constructor(
settings.isAllFavouritesVisible = isVisible settings.isAllFavouritesVisible = isVisible
} }
fun isInReorderMode(): Boolean = isInReorderMode.value
fun isEmpty(): Boolean = detalizedCategories.value.none { it is CategoryListModel } fun isEmpty(): Boolean = detalizedCategories.value.none { it is CategoryListModel }
fun setReorderMode(isReorderMode: Boolean) {
isInReorderMode.value = isReorderMode
}
fun reorderCategories(oldPos: Int, newPos: Int) { fun reorderCategories(oldPos: Int, newPos: Int) {
val prevJob = reorderJob val prevJob = reorderJob
reorderJob = launchJob(Dispatchers.Default) { reorderJob = launchJob(Dispatchers.Default) {

@ -1,5 +1,6 @@
package org.koitharu.kotatsu.favourites.ui.categories.adapter package org.koitharu.kotatsu.favourites.ui.categories.adapter
import android.annotation.SuppressLint
import android.content.res.ColorStateList import android.content.res.ColorStateList
import android.graphics.Color import android.graphics.Color
import android.graphics.drawable.ColorDrawable import android.graphics.drawable.ColorDrawable
@ -25,6 +26,7 @@ import org.koitharu.kotatsu.databinding.ItemCategoryBinding
import org.koitharu.kotatsu.favourites.ui.categories.FavouriteCategoriesListListener import org.koitharu.kotatsu.favourites.ui.categories.FavouriteCategoriesListListener
import org.koitharu.kotatsu.list.ui.model.ListModel import org.koitharu.kotatsu.list.ui.model.ListModel
@SuppressLint("ClickableViewAccessibility")
fun categoryAD( fun categoryAD(
coil: ImageLoader, coil: ImageLoader,
lifecycleOwner: LifecycleOwner, lifecycleOwner: LifecycleOwner,
@ -35,8 +37,7 @@ fun categoryAD(
val eventListener = object : OnClickListener, OnLongClickListener, OnTouchListener { val eventListener = object : OnClickListener, OnLongClickListener, OnTouchListener {
override fun onClick(v: View) = clickListener.onItemClick(item.category, binding.imageViewCover1) override fun onClick(v: View) = clickListener.onItemClick(item.category, binding.imageViewCover1)
override fun onLongClick(v: View) = clickListener.onItemLongClick(item.category, binding.imageViewCover1) override fun onLongClick(v: View) = clickListener.onItemLongClick(item.category, binding.imageViewCover1)
override fun onTouch(v: View?, event: MotionEvent): Boolean = item.isReorderMode && override fun onTouch(v: View?, event: MotionEvent): Boolean = event.actionMasked == MotionEvent.ACTION_DOWN &&
event.actionMasked == MotionEvent.ACTION_DOWN &&
clickListener.onDragHandleTouch(this@adapterDelegateViewBinding) clickListener.onDragHandleTouch(this@adapterDelegateViewBinding)
} }
val backgroundColor = context.getThemeColor(android.R.attr.colorBackground) val backgroundColor = context.getThemeColor(android.R.attr.colorBackground)
@ -57,10 +58,9 @@ fun categoryAD(
val crossFadeDuration = context.getAnimationDuration(R.integer.config_defaultAnimTime).toInt() val crossFadeDuration = context.getAnimationDuration(R.integer.config_defaultAnimTime).toInt()
itemView.setOnClickListener(eventListener) itemView.setOnClickListener(eventListener)
itemView.setOnLongClickListener(eventListener) itemView.setOnLongClickListener(eventListener)
itemView.setOnTouchListener(eventListener) binding.imageViewHandle.setOnTouchListener(eventListener)
bind { payloads -> bind { payloads ->
binding.imageViewHandle.isVisible = item.isReorderMode
if (payloads.isNotEmpty()) { if (payloads.isNotEmpty()) {
return@bind return@bind
} }
@ -74,6 +74,8 @@ fun categoryAD(
item.mangaCount, item.mangaCount,
) )
} }
binding.imageViewTracker.isVisible = item.category.isTrackingEnabled
binding.imageViewVisible.isVisible = item.category.isVisibleInLibrary
repeat(coverViews.size) { i -> repeat(coverViews.size) { i ->
val cover = item.covers.getOrNull(i) val cover = item.covers.getOrNull(i)
coverViews[i].newImageRequest(lifecycleOwner, cover?.url)?.run { coverViews[i].newImageRequest(lifecycleOwner, cover?.url)?.run {

@ -8,7 +8,6 @@ class CategoryListModel(
val mangaCount: Int, val mangaCount: Int,
val covers: List<Cover>, val covers: List<Cover>,
val category: FavouriteCategory, val category: FavouriteCategory,
val isReorderMode: Boolean,
) : ListModel { ) : ListModel {
override fun areItemsTheSame(other: ListModel): Boolean { override fun areItemsTheSame(other: ListModel): Boolean {
@ -22,20 +21,26 @@ class CategoryListModel(
other as CategoryListModel other as CategoryListModel
if (mangaCount != other.mangaCount) return false if (mangaCount != other.mangaCount) return false
if (isReorderMode != other.isReorderMode) return false
if (covers != other.covers) return false if (covers != other.covers) return false
if (category.id != other.category.id) return false if (category.id != other.category.id) return false
if (category.title != other.category.title) return false if (category.title != other.category.title) return false
return category.order == other.category.order // ignore the category.sortKey field
if (category.order != other.category.order) return false
if (category.createdAt != other.category.createdAt) return false
if (category.isTrackingEnabled != other.category.isTrackingEnabled) return false
return category.isVisibleInLibrary == other.category.isVisibleInLibrary
} }
override fun hashCode(): Int { override fun hashCode(): Int {
var result = mangaCount var result = mangaCount
result = 31 * result + isReorderMode.hashCode()
result = 31 * result + covers.hashCode() result = 31 * result + covers.hashCode()
result = 31 * result + category.id.hashCode() result = 31 * result + category.id.hashCode()
result = 31 * result + category.title.hashCode() result = 31 * result + category.title.hashCode()
// ignore the category.sortKey field
result = 31 * result + category.order.hashCode() result = 31 * result + category.order.hashCode()
result = 31 * result + category.createdAt.hashCode()
result = 31 * result + category.isTrackingEnabled.hashCode()
result = 31 * result + category.isVisibleInLibrary.hashCode()
return result return result
} }
} }

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?><!-- drawable/eye_outline.xml -->
<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="#000000"
android:pathData="M12,9A3,3 0 0,1 15,12A3,3 0 0,1 12,15A3,3 0 0,1 9,12A3,3 0 0,1 12,9M12,4.5C17,4.5 21.27,7.61 23,12C21.27,16.39 17,19.5 12,19.5C7,19.5 2.73,16.39 1,12C2.73,7.61 7,4.5 12,4.5M3.18,12C4.83,15.36 8.24,17.5 12,17.5C15.76,17.5 19.17,15.36 20.82,12C19.17,8.64 15.76,6.5 12,6.5C8.24,6.5 4.83,8.64 3.18,12Z" />
</vector>

@ -84,16 +84,42 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="@dimen/margin_normal" android:layout_marginStart="@dimen/margin_normal"
android:layout_marginTop="4dp" android:layout_marginTop="4dp"
android:layout_marginEnd="?listPreferredItemPaddingEnd" android:layout_marginEnd="8dp"
android:singleLine="true" android:singleLine="true"
android:textAppearance="?attr/textAppearanceBodySmall" android:textAppearance="?attr/textAppearanceBodySmall"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/imageView_handle" app:layout_constraintEnd_toStartOf="@id/imageView_tracker"
app:layout_constraintHorizontal_bias="0"
app:layout_constraintHorizontal_chainStyle="packed"
app:layout_constraintStart_toEndOf="@id/imageView_cover3" app:layout_constraintStart_toEndOf="@id/imageView_cover3"
app:layout_constraintTop_toBottomOf="@id/textView_title" app:layout_constraintTop_toBottomOf="@id/textView_title"
app:layout_constraintVertical_chainStyle="packed" app:layout_constraintVertical_chainStyle="packed"
app:layout_constraintWidth_default="wrap"
tools:text="@tools:sample/lorem[1]" /> tools:text="@tools:sample/lorem[1]" />
<ImageView
android:id="@+id/imageView_tracker"
android:layout_width="16dp"
android:layout_height="16dp"
android:layout_marginEnd="4dp"
android:contentDescription="@string/check_for_new_chapters"
app:layout_constraintBottom_toBottomOf="@id/textView_subtitle"
app:layout_constraintEnd_toStartOf="@id/imageView_visible"
app:layout_constraintStart_toEndOf="@id/textView_subtitle"
app:layout_constraintTop_toTopOf="@id/textView_subtitle"
app:srcCompat="@drawable/ic_notification" />
<ImageView
android:id="@+id/imageView_visible"
android:layout_width="16dp"
android:layout_height="16dp"
android:contentDescription="@string/show_on_shelf"
app:layout_constraintBottom_toBottomOf="@id/textView_subtitle"
app:layout_constraintEnd_toEndOf="@id/textView_title"
app:layout_constraintStart_toEndOf="@id/imageView_tracker"
app:layout_constraintTop_toTopOf="@id/textView_subtitle"
app:srcCompat="@drawable/ic_eye" />
<ImageView <ImageView
android:id="@+id/imageView_handle" android:id="@+id/imageView_handle"
android:layout_width="wrap_content" android:layout_width="wrap_content"
@ -102,10 +128,8 @@
android:contentDescription="@string/reorder" android:contentDescription="@string/reorder"
android:padding="@dimen/margin_normal" android:padding="@dimen/margin_normal"
android:src="@drawable/ic_reorder_handle" android:src="@drawable/ic_reorder_handle"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent" />
tools:visibility="visible" />
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>

@ -1,12 +0,0 @@
<?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_reorder"
android:icon="@drawable/ic_reorder"
android:title="@string/reorder"
app:showAsAction="ifRoom" />
</menu>
Loading…
Cancel
Save