Migrate favourite categories to AdapterDelegates

This commit is contained in:
Koitharu
2020-11-28 08:30:49 +02:00
parent fa02cfd7e8
commit 53e36d23b1
14 changed files with 102 additions and 122 deletions

View File

@@ -21,7 +21,7 @@ class ChaptersSelectionDecoration(context: Context) : RecyclerView.ItemDecoratio
private val paint = Paint(Paint.ANTI_ALIAS_FLAG) private val paint = Paint(Paint.ANTI_ALIAS_FLAG)
init { init {
paint.color = context.getThemeColor(android.R.attr.colorControlActivated) paint.color = context.getThemeColor(com.google.android.material.R.attr.colorSurface)
paint.style = Paint.Style.FILL paint.style = Paint.Style.FILL
} }

View File

@@ -1,6 +1,8 @@
package org.koitharu.kotatsu.favourites.data package org.koitharu.kotatsu.favourites.data
import androidx.room.* import androidx.room.*
import kotlinx.coroutines.flow.Flow
import org.koitharu.kotatsu.core.model.FavouriteCategory
@Dao @Dao
abstract class FavouriteCategoriesDao { abstract class FavouriteCategoriesDao {
@@ -8,6 +10,9 @@ abstract class FavouriteCategoriesDao {
@Query("SELECT * FROM favourite_categories ORDER BY sort_key") @Query("SELECT * FROM favourite_categories ORDER BY sort_key")
abstract suspend fun findAll(): List<FavouriteCategoryEntity> abstract suspend fun findAll(): List<FavouriteCategoryEntity>
@Query("SELECT * FROM favourite_categories ORDER BY sort_key")
abstract fun observeAll(): Flow<List<FavouriteCategoryEntity>>
@Insert(onConflict = OnConflictStrategy.ABORT) @Insert(onConflict = OnConflictStrategy.ABORT)
abstract suspend fun insert(category: FavouriteCategoryEntity): Long abstract suspend fun insert(category: FavouriteCategoryEntity): Long

View File

@@ -58,6 +58,12 @@ class FavouritesRepository(private val db: MangaDatabase) {
return entities?.map { it.toFavouriteCategory() }.orEmpty() return entities?.map { it.toFavouriteCategory() }.orEmpty()
} }
fun observeCategories(): Flow<List<FavouriteCategory>> {
return db.favouriteCategoriesDao.observeAll().mapItems {
it.toFavouriteCategory()
}
}
fun observeCategories(mangaId: Long): Flow<List<FavouriteCategory>> { fun observeCategories(mangaId: Long): Flow<List<FavouriteCategory>> {
return db.favouritesDao.observe(mangaId).map { entity -> return db.favouritesDao.observe(mangaId).map { entity ->
entity?.categories?.map { it.toFavouriteCategory() }.orEmpty() entity?.categories?.map { it.toFavouriteCategory() }.orEmpty()

View File

@@ -1,5 +1,6 @@
package org.koitharu.kotatsu.favourites.domain package org.koitharu.kotatsu.favourites.domain
@Deprecated("Use flow")
fun interface OnFavouritesChangeListener { fun interface OnFavouritesChangeListener {
fun onFavouritesChanged(mangaId: Long) fun onFavouritesChanged(mangaId: Long)

View File

@@ -12,8 +12,6 @@ import org.koin.android.viewmodel.ext.android.viewModel
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.BaseFragment import org.koitharu.kotatsu.base.ui.BaseFragment
import org.koitharu.kotatsu.core.model.FavouriteCategory import org.koitharu.kotatsu.core.model.FavouriteCategory
import org.koitharu.kotatsu.favourites.domain.FavouritesRepository
import org.koitharu.kotatsu.favourites.domain.OnFavouritesChangeListener
import org.koitharu.kotatsu.favourites.ui.categories.CategoriesActivity import org.koitharu.kotatsu.favourites.ui.categories.CategoriesActivity
import org.koitharu.kotatsu.favourites.ui.categories.CategoriesEditDelegate import org.koitharu.kotatsu.favourites.ui.categories.CategoriesEditDelegate
import org.koitharu.kotatsu.favourites.ui.categories.FavouritesCategoriesViewModel import org.koitharu.kotatsu.favourites.ui.categories.FavouritesCategoriesViewModel
@@ -22,8 +20,7 @@ import java.util.*
import kotlin.collections.ArrayList import kotlin.collections.ArrayList
class FavouritesContainerFragment : BaseFragment(R.layout.fragment_favourites), class FavouritesContainerFragment : BaseFragment(R.layout.fragment_favourites),
OnFavouritesChangeListener, FavouritesTabLongClickListener, FavouritesTabLongClickListener, CategoriesEditDelegate.CategoriesEditCallback {
CategoriesEditDelegate.CategoriesEditCallback {
private val viewModel by viewModel<FavouritesCategoriesViewModel>() private val viewModel by viewModel<FavouritesCategoriesViewModel>()
@@ -41,18 +38,12 @@ class FavouritesContainerFragment : BaseFragment(R.layout.fragment_favourites),
val adapter = FavouritesPagerAdapter(this, this) val adapter = FavouritesPagerAdapter(this, this)
pager.adapter = adapter pager.adapter = adapter
TabLayoutMediator(tabs, pager, adapter).attach() TabLayoutMediator(tabs, pager, adapter).attach()
FavouritesRepository.subscribe(this)
viewModel.categories.observe(viewLifecycleOwner, ::onCategoriesChanged) viewModel.categories.observe(viewLifecycleOwner, ::onCategoriesChanged)
viewModel.onError.observe(viewLifecycleOwner, ::onError) viewModel.onError.observe(viewLifecycleOwner, ::onError)
} }
override fun onDestroyView() { private fun onCategoriesChanged(categories: List<FavouriteCategory>) {
FavouritesRepository.unsubscribe(this)
super.onDestroyView()
}
fun onCategoriesChanged(categories: List<FavouriteCategory>) {
val data = ArrayList<FavouriteCategory>(categories.size + 1) val data = ArrayList<FavouriteCategory>(categories.size + 1)
data += FavouriteCategory(0L, getString(R.string.all_favourites), -1, Date()) data += FavouriteCategory(0L, getString(R.string.all_favourites), -1, Date())
data += categories data += categories
@@ -75,19 +66,13 @@ class FavouritesContainerFragment : BaseFragment(R.layout.fragment_favourites),
} }
override fun getTitle(): CharSequence? { override fun getTitle(): CharSequence? {
return getString(R.string.favourites) return context?.getString(R.string.favourites)
} }
private fun onError(e: Throwable) { private fun onError(e: Throwable) {
Snackbar.make(pager, e.message ?: return, Snackbar.LENGTH_LONG).show() Snackbar.make(pager, e.message ?: return, Snackbar.LENGTH_LONG).show()
} }
override fun onFavouritesChanged(mangaId: Long) = Unit
override fun onCategoriesChanged() {
viewModel.loadAllCategories()
}
override fun onTabLongClick(tabView: View, category: FavouriteCategory): Boolean { override fun onTabLongClick(tabView: View, category: FavouriteCategory): Boolean {
val menuRes = if (category.id == 0L) R.menu.popup_category_empty else R.menu.popup_category val menuRes = if (category.id == 0L) R.menu.popup_category_empty else R.menu.popup_category
tabView.showPopupMenu(menuRes) { tabView.showPopupMenu(menuRes) {

View File

@@ -15,12 +15,12 @@ import kotlinx.android.synthetic.main.activity_categories.*
import org.koin.android.viewmodel.ext.android.viewModel import org.koin.android.viewmodel.ext.android.viewModel
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.BaseActivity import org.koitharu.kotatsu.base.ui.BaseActivity
import org.koitharu.kotatsu.base.ui.list.OnRecyclerItemClickListener import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.model.FavouriteCategory import org.koitharu.kotatsu.core.model.FavouriteCategory
import org.koitharu.kotatsu.utils.ext.getDisplayMessage import org.koitharu.kotatsu.utils.ext.getDisplayMessage
import org.koitharu.kotatsu.utils.ext.showPopupMenu import org.koitharu.kotatsu.utils.ext.showPopupMenu
class CategoriesActivity : BaseActivity(), OnRecyclerItemClickListener<FavouriteCategory>, class CategoriesActivity : BaseActivity(), OnListItemClickListener<FavouriteCategory>,
View.OnClickListener, CategoriesEditDelegate.CategoriesEditCallback { View.OnClickListener, CategoriesEditDelegate.CategoriesEditCallback {
private val viewModel by viewModel<FavouritesCategoriesViewModel>() private val viewModel by viewModel<FavouritesCategoriesViewModel>()
@@ -52,7 +52,7 @@ class CategoriesActivity : BaseActivity(), OnRecyclerItemClickListener<Favourite
} }
} }
override fun onItemClick(item: FavouriteCategory, position: Int, view: View) { override fun onItemClick(item: FavouriteCategory, view: View) {
view.showPopupMenu(R.menu.popup_category) { view.showPopupMenu(R.menu.popup_category) {
when (it.itemId) { when (it.itemId) {
R.id.action_remove -> editDelegate.deleteCategory(item) R.id.action_remove -> editDelegate.deleteCategory(item)
@@ -62,15 +62,15 @@ class CategoriesActivity : BaseActivity(), OnRecyclerItemClickListener<Favourite
} }
} }
override fun onItemLongClick(item: FavouriteCategory, position: Int, view: View): Boolean { override fun onItemLongClick(item: FavouriteCategory, view: View): Boolean {
reorderHelper.startDrag( reorderHelper.startDrag(
recyclerView.findViewHolderForAdapterPosition(position) ?: return false recyclerView.findContainingViewHolder(view) ?: return false
) )
return true return true
} }
private fun onCategoriesChanged(categories: List<FavouriteCategory>) { private fun onCategoriesChanged(categories: List<FavouriteCategory>) {
adapter.replaceData(categories) adapter.items = categories
textView_holder.isVisible = categories.isEmpty() textView_holder.isVisible = categories.isEmpty()
} }
@@ -102,8 +102,7 @@ class CategoriesActivity : BaseActivity(), OnRecyclerItemClickListener<Favourite
): Boolean { ): Boolean {
val oldPos = viewHolder.bindingAdapterPosition val oldPos = viewHolder.bindingAdapterPosition
val newPos = target.bindingAdapterPosition val newPos = target.bindingAdapterPosition
adapter.moveItem(oldPos, newPos) viewModel.reorderCategories(oldPos, newPos)
viewModel.storeCategoriesOrder(adapter.items.map { it.id })
return true return true
} }

View File

@@ -1,49 +1,27 @@
package org.koitharu.kotatsu.favourites.ui.categories package org.koitharu.kotatsu.favourites.ui.categories
import android.annotation.SuppressLint import androidx.recyclerview.widget.DiffUtil
import android.view.MotionEvent import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter
import android.view.ViewGroup import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
import kotlinx.android.synthetic.main.item_category.*
import org.koitharu.kotatsu.base.ui.list.BaseRecyclerAdapter
import org.koitharu.kotatsu.base.ui.list.BaseViewHolder
import org.koitharu.kotatsu.base.ui.list.OnRecyclerItemClickListener
import org.koitharu.kotatsu.core.model.FavouriteCategory import org.koitharu.kotatsu.core.model.FavouriteCategory
import kotlin.jvm.internal.Intrinsics
class CategoriesAdapter(private val onItemClickListener: OnRecyclerItemClickListener<FavouriteCategory>) : class CategoriesAdapter(
BaseRecyclerAdapter<FavouriteCategory, Unit>() { onItemClickListener: OnListItemClickListener<FavouriteCategory>
) : AsyncListDifferDelegationAdapter<FavouriteCategory>(DiffCallback()) {
override fun onCreateViewHolder(parent: ViewGroup) = CategoryHolder(parent) init {
delegatesManager.addDelegate(categoryAD(onItemClickListener))
override fun onGetItemId(item: FavouriteCategory) = item.id
override fun getExtra(item: FavouriteCategory, position: Int) = Unit
@SuppressLint("ClickableViewAccessibility")
override fun onViewAttachedToWindow(holder: BaseViewHolder<FavouriteCategory, Unit>) {
holder.imageView_more.setOnClickListener { v ->
onItemClickListener.onItemClick(holder.requireData(), holder.bindingAdapterPosition, v)
}
holder.imageView_handle.setOnTouchListener { v, event ->
if (event.actionMasked == MotionEvent.ACTION_DOWN) {
onItemClickListener.onItemLongClick(
holder.requireData(),
holder.bindingAdapterPosition,
v
)
} else {
false
}
}
} }
override fun onViewDetachedFromWindow(holder: BaseViewHolder<FavouriteCategory, Unit>) { private class DiffCallback : DiffUtil.ItemCallback<FavouriteCategory>() {
holder.imageView_more.setOnClickListener(null)
holder.imageView_handle.setOnTouchListener(null)
}
fun moveItem(oldPos: Int, newPos: Int) { override fun areItemsTheSame(oldItem: FavouriteCategory, newItem: FavouriteCategory): Boolean {
val item = dataSet.removeAt(oldPos) return oldItem.id == newItem.id
dataSet.add(newPos, item) }
notifyItemMoved(oldPos, newPos)
override fun areContentsTheSame(oldItem: FavouriteCategory, newItem: FavouriteCategory): Boolean {
return Intrinsics.areEqual(oldItem, newItem)
}
} }
} }

View File

@@ -0,0 +1,29 @@
package org.koitharu.kotatsu.favourites.ui.categories
import android.view.MotionEvent
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateLayoutContainer
import kotlinx.android.synthetic.main.item_category.*
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.model.FavouriteCategory
fun categoryAD(
clickListener: OnListItemClickListener<FavouriteCategory>
) = adapterDelegateLayoutContainer<FavouriteCategory, FavouriteCategory>(R.layout.item_category) {
imageView_more.setOnClickListener {
clickListener.onItemClick(item, it)
}
@Suppress("ClickableViewAccessibility")
imageView_handle.setOnTouchListener { v, event ->
if (event.actionMasked == MotionEvent.ACTION_DOWN) {
clickListener.onItemLongClick(item, v)
} else {
false
}
}
bind {
textView_title.text = item.title
}
}

View File

@@ -1,15 +0,0 @@
package org.koitharu.kotatsu.favourites.ui.categories
import android.view.ViewGroup
import kotlinx.android.synthetic.main.item_category.*
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.list.BaseViewHolder
import org.koitharu.kotatsu.core.model.FavouriteCategory
class CategoryHolder(parent: ViewGroup) :
BaseViewHolder<FavouriteCategory, Unit>(parent, R.layout.item_category) {
override fun onBind(data: FavouriteCategory, extra: Unit) {
textView_title.text = data.title
}
}

View File

@@ -1,9 +1,15 @@
package org.koitharu.kotatsu.favourites.ui.categories package org.koitharu.kotatsu.favourites.ui.categories
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.asLiveData
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.plus
import org.koitharu.kotatsu.base.ui.BaseViewModel import org.koitharu.kotatsu.base.ui.BaseViewModel
import org.koitharu.kotatsu.core.model.FavouriteCategory
import org.koitharu.kotatsu.core.model.Manga import org.koitharu.kotatsu.core.model.Manga
import org.koitharu.kotatsu.favourites.domain.FavouritesRepository import org.koitharu.kotatsu.favourites.domain.FavouritesRepository
import org.koitharu.kotatsu.utils.ext.mapToSet import org.koitharu.kotatsu.utils.ext.mapToSet
@@ -13,53 +19,47 @@ class FavouritesCategoriesViewModel(
) : BaseViewModel() { ) : BaseViewModel() {
private var reorderJob: Job? = null private var reorderJob: Job? = null
private var mangaSubscription: Job? = null
val categories = MutableLiveData<List<FavouriteCategory>>() val categories = repository.observeCategories()
val mangaCategories = MutableLiveData<Set<Int>>() .asLiveData(viewModelScope.coroutineContext + Dispatchers.Default)
val mangaCategories = MutableLiveData<Set<Long>>(emptySet())
init { fun observeMangaCategories(mangaId: Long) {
loadAllCategories() mangaSubscription?.cancel()
} mangaSubscription = repository.observeCategories(mangaId)
.map { list -> list.mapToSet { it.id } }
fun loadAllCategories() { .onEach { mangaCategories.postValue(it) }
launchJob { .launchIn(viewModelScope + Dispatchers.Default)
categories.value = repository.getAllCategories()
}
}
fun loadMangaCategories(manga: Manga) {
launchJob {
val categories = repository.getCategories(manga.id)
mangaCategories.value = categories.mapToSet { it.id.toInt() }
}
} }
fun createCategory(name: String) { fun createCategory(name: String) {
launchJob { launchJob {
repository.addCategory(name) repository.addCategory(name)
categories.value = repository.getAllCategories()
} }
} }
fun renameCategory(id: Long, name: String) { fun renameCategory(id: Long, name: String) {
launchJob { launchJob {
repository.renameCategory(id, name) repository.renameCategory(id, name)
categories.value = repository.getAllCategories()
} }
} }
fun deleteCategory(id: Long) { fun deleteCategory(id: Long) {
launchJob { launchJob {
repository.removeCategory(id) repository.removeCategory(id)
categories.value = repository.getAllCategories()
} }
} }
fun storeCategoriesOrder(orderedIds: List<Long>) { fun reorderCategories(oldPos: Int, newPos: Int) {
val prevJob = reorderJob val prevJob = reorderJob
reorderJob = launchJob { reorderJob = launchJob {
prevJob?.join() prevJob?.join()
repository.reorderCategories(orderedIds) val items = categories.value ?: error("This should not happen")
val ids = items.mapTo(ArrayList(items.size)) { it.id }
val item = ids.removeAt(oldPos)
ids.add(newPos, item)
repository.reorderCategories(ids)
} }
} }

View File

@@ -1,9 +1,8 @@
package org.koitharu.kotatsu.favourites.ui.categories.select package org.koitharu.kotatsu.favourites.ui.categories.select
import android.util.SparseBooleanArray
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.Checkable import android.widget.Checkable
import androidx.core.util.set import androidx.collection.ArraySet
import org.koitharu.kotatsu.base.ui.list.BaseRecyclerAdapter import org.koitharu.kotatsu.base.ui.list.BaseRecyclerAdapter
import org.koitharu.kotatsu.base.ui.list.BaseViewHolder import org.koitharu.kotatsu.base.ui.list.BaseViewHolder
import org.koitharu.kotatsu.core.model.FavouriteCategory import org.koitharu.kotatsu.core.model.FavouriteCategory
@@ -11,18 +10,15 @@ import org.koitharu.kotatsu.core.model.FavouriteCategory
class CategoriesSelectAdapter(private val listener: OnCategoryCheckListener) : class CategoriesSelectAdapter(private val listener: OnCategoryCheckListener) :
BaseRecyclerAdapter<FavouriteCategory, Boolean>() { BaseRecyclerAdapter<FavouriteCategory, Boolean>() {
private val checkedIds = SparseBooleanArray() private val checkedIds = ArraySet<Long>()
fun setCheckedIds(ids: Iterable<Int>) { fun setCheckedIds(ids: Iterable<Long>) {
checkedIds.clear() checkedIds.clear()
ids.forEach { checkedIds.addAll(ids)
checkedIds[it] = true
}
notifyDataSetChanged() notifyDataSetChanged()
} }
override fun getExtra(item: FavouriteCategory, position: Int) = override fun getExtra(item: FavouriteCategory, position: Int) = item.id in checkedIds
checkedIds.get(item.id.toInt(), false)
override fun onCreateViewHolder(parent: ViewGroup) = override fun onCreateViewHolder(parent: ViewGroup) =
CategoryCheckableHolder( CategoryCheckableHolder(

View File

@@ -36,7 +36,7 @@ class FavouriteCategoriesDialog : BaseBottomSheet(R.layout.dialog_favorite_categ
createCategory() createCategory()
} }
manga?.let { manga?.let {
viewModel.loadMangaCategories(it) viewModel.observeMangaCategories(it.id)
} }
viewModel.categories.observe(viewLifecycleOwner, ::onCategoriesChanged) viewModel.categories.observe(viewLifecycleOwner, ::onCategoriesChanged)
@@ -53,7 +53,7 @@ class FavouriteCategoriesDialog : BaseBottomSheet(R.layout.dialog_favorite_categ
adapter?.replaceData(categories) adapter?.replaceData(categories)
} }
private fun onCheckedCategoriesChanged(checkedIds: Set<Int>) { private fun onCheckedCategoriesChanged(checkedIds: Set<Long>) {
adapter?.setCheckedIds(checkedIds) adapter?.setCheckedIds(checkedIds)
} }

View File

@@ -44,9 +44,6 @@ class FeedFragment : BaseFragment(R.layout.fragment_tracklogs), PaginationScroll
) )
recyclerView.setHasFixedSize(true) recyclerView.setHasFixedSize(true)
recyclerView.addOnScrollListener(PaginationScrollListener(4, this)) recyclerView.addOnScrollListener(PaginationScrollListener(4, this))
if (savedInstanceState == null) {
onScrolledToEnd()
}
viewModel.content.observe(viewLifecycleOwner, this::onListChanged) viewModel.content.observe(viewLifecycleOwner, this::onListChanged)
viewModel.isLoading.observe(viewLifecycleOwner, this::onLoadingStateChanged) viewModel.isLoading.observe(viewLifecycleOwner, this::onLoadingStateChanged)

View File

@@ -9,7 +9,6 @@ import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.drop import kotlinx.coroutines.flow.drop
import kotlinx.coroutines.flow.onEach
import org.koitharu.kotatsu.base.ui.BaseViewModel import org.koitharu.kotatsu.base.ui.BaseViewModel
import org.koitharu.kotatsu.core.model.TrackingLogItem import org.koitharu.kotatsu.core.model.TrackingLogItem
import org.koitharu.kotatsu.list.ui.model.IndeterminateProgress import org.koitharu.kotatsu.list.ui.model.IndeterminateProgress
@@ -28,9 +27,7 @@ class FeedViewModel(
val isEmptyState = MutableLiveData(false) val isEmptyState = MutableLiveData(false)
val content = combine( val content = combine(
logList.drop(1).onEach { logList.drop(1).mapItems {
isEmptyState.postValue(it.isEmpty())
}.mapItems {
it.toFeedItem(context.resources) it.toFeedItem(context.resources)
}, },
hasNextPage hasNextPage
@@ -53,6 +50,8 @@ class FeedViewModel(
logList.value = list logList.value = list
} else if (list.isNotEmpty()) { } else if (list.isNotEmpty()) {
logList.value += list logList.value += list
} else {
isEmptyState.value = true
} }
hasNextPage.value = list.isNotEmpty() hasNextPage.value = list.isNotEmpty()
} }