Fully migrate to AdapterDelegates and cleanup code

This commit is contained in:
Koitharu
2020-11-28 11:05:27 +02:00
parent 53e36d23b1
commit 5ed4d0b6b7
48 changed files with 416 additions and 479 deletions

View File

@@ -29,6 +29,7 @@ import org.koitharu.kotatsu.search.searchModule
import org.koitharu.kotatsu.settings.settingsModule
import org.koitharu.kotatsu.tracker.trackerModule
import org.koitharu.kotatsu.widget.WidgetUpdater
import org.koitharu.kotatsu.widget.appWidgetModule
class KotatsuApp : Application() {
@@ -77,7 +78,8 @@ class KotatsuApp : Application() {
detailsModule,
trackerModule,
settingsModule,
readerModule
readerModule,
appWidgetModule
)
}
}

View File

@@ -8,7 +8,6 @@ import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import androidx.appcompat.app.AlertDialog
import com.google.android.material.checkbox.MaterialCheckBox
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import org.koitharu.kotatsu.R
class CheckBoxAlertDialog private constructor(private val delegate: AlertDialog) :
@@ -23,7 +22,7 @@ class CheckBoxAlertDialog private constructor(private val delegate: AlertDialog)
.inflate(R.layout.dialog_checkbox, null, false)
private val checkBox = view.findViewById<MaterialCheckBox>(android.R.id.checkbox)
private val delegate = MaterialAlertDialogBuilder(context)
private val delegate = AlertDialog.Builder(context)
.setView(view)
fun setTitle(@StringRes titleResId: Int): Builder {

View File

@@ -7,7 +7,6 @@ import android.view.ViewGroup
import android.widget.BaseAdapter
import androidx.annotation.StringRes
import androidx.appcompat.app.AlertDialog
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import kotlinx.android.synthetic.main.item_storage.view.*
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.local.domain.LocalMangaRepository
@@ -24,7 +23,7 @@ class StorageSelectDialog private constructor(private val delegate: AlertDialog)
class Builder(context: Context, defaultValue: File?, listener: OnStorageSelectListener) {
private val adapter = VolumesAdapter(context)
private val delegate = MaterialAlertDialogBuilder(context)
private val delegate = AlertDialog.Builder(context)
init {
if (adapter.isEmpty) {

View File

@@ -7,7 +7,6 @@ import android.text.InputFilter
import android.view.LayoutInflater
import androidx.annotation.StringRes
import androidx.appcompat.app.AlertDialog
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import kotlinx.android.synthetic.main.dialog_input.view.*
import org.koitharu.kotatsu.R
@@ -23,7 +22,7 @@ class TextInputDialog private constructor(
private val view = LayoutInflater.from(context)
.inflate(R.layout.dialog_input, null, false)
private val delegate = MaterialAlertDialogBuilder(context)
private val delegate = AlertDialog.Builder(context)
.setView(view)
fun setTitle(@StringRes titleResId: Int): Builder {

View File

@@ -4,6 +4,7 @@ import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
import java.util.*
@Deprecated("")
class AdapterUpdater<T>(oldList: List<T>, newList: List<T>, getId: (T) -> Long) {
private val diff = DiffUtil.calculateDiff(object : DiffUtil.Callback() {

View File

@@ -1,110 +0,0 @@
package org.koitharu.kotatsu.base.ui.list
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import org.koin.core.component.KoinComponent
import org.koitharu.kotatsu.utils.ext.replaceWith
@Deprecated("", replaceWith = ReplaceWith("AsyncListDifferDelegationAdapter"))
abstract class BaseRecyclerAdapter<T, E>(private val onItemClickListener: OnRecyclerItemClickListener<T>? = null) :
RecyclerView.Adapter<BaseViewHolder<T, E>>(),
KoinComponent {
protected val dataSet = ArrayList<T>() //TODO make private
val items get() = dataSet as List<T>
val hasItems get() = dataSet.isNotEmpty()
init {
@Suppress("LeakingThis")
setHasStableIds(true)
}
override fun onBindViewHolder(holder: BaseViewHolder<T, E>, position: Int) {
val item = dataSet[position]
holder.bind(item, getExtra(item, position))
}
fun getItem(position: Int) = dataSet[position]
override fun getItemId(position: Int) = onGetItemId(dataSet[position])
protected fun findItemById(id: Long) = dataSet.find { x -> onGetItemId(x) == id }
protected fun findItemPositionById(id: Long) =
dataSet.indexOfFirst { x -> onGetItemId(x) == id }
fun replaceData(newData: List<T>) {
val updater = AdapterUpdater(dataSet, newData, this::onGetItemId)
dataSet.replaceWith(newData)
updater(this)
onDataSetChanged()
}
fun appendData(newData: List<T>) {
val pos = dataSet.size
dataSet.addAll(newData)
notifyItemRangeInserted(pos, newData.size)
onDataSetChanged()
}
fun prependData(newData: List<T>) {
dataSet.addAll(0, newData)
notifyItemRangeInserted(0, newData.size)
onDataSetChanged()
}
fun appendItem(newItem: T) {
dataSet.add(newItem)
notifyItemInserted(dataSet.lastIndex)
onDataSetChanged()
}
fun removeItem(item: T) {
removeItemAt(dataSet.indexOf(item))
onDataSetChanged()
}
fun removeItemAt(position: Int) {
if (position in dataSet.indices) {
dataSet.removeAt(position)
notifyItemRemoved(position)
}
onDataSetChanged()
}
fun clearData() {
dataSet.clear()
notifyDataSetChanged()
onDataSetChanged()
}
override fun onViewRecycled(holder: BaseViewHolder<T, E>) {
holder.onRecycled()
}
final override fun getItemCount() = dataSet.size
final override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BaseViewHolder<T, E> {
return onCreateViewHolder(parent)
}
override fun onViewDetachedFromWindow(holder: BaseViewHolder<T, E>) {
holder.setOnItemClickListener(null)
super.onViewDetachedFromWindow(holder)
}
override fun onViewAttachedToWindow(holder: BaseViewHolder<T, E>) {
super.onViewAttachedToWindow(holder)
holder.setOnItemClickListener(onItemClickListener)
}
protected open fun onDataSetChanged() = Unit
protected abstract fun getExtra(item: T, position: Int): E
protected abstract fun onCreateViewHolder(parent: ViewGroup): BaseViewHolder<T, E>
protected abstract fun onGetItemId(item: T): Long
}

View File

@@ -30,25 +30,7 @@ abstract class BaseViewHolder<T, E> protected constructor(view: View) :
return boundData ?: throw IllegalStateException("Calling requireData() before bind()")
}
fun setOnItemClickListener(listener: OnRecyclerItemClickListener<T>?) {
val listenersAdapter = listener?.let { HolderListenersAdapter(it) }
itemView.setOnClickListener(listenersAdapter)
itemView.setOnLongClickListener(listenersAdapter)
}
open fun onRecycled() = Unit
abstract fun onBind(data: T, extra: E)
private inner class HolderListenersAdapter(private val listener: OnRecyclerItemClickListener<T>) :
View.OnClickListener, View.OnLongClickListener {
override fun onClick(v: View) {
listener.onItemClick(boundData ?: return, bindingAdapterPosition, v)
}
override fun onLongClick(v: View): Boolean {
return listener.onItemLongClick(boundData ?: return false, bindingAdapterPosition, v)
}
}
}

View File

@@ -1,10 +0,0 @@
package org.koitharu.kotatsu.base.ui.list
import android.view.View
interface OnRecyclerItemClickListener<I> {
fun onItemClick(item: I, position: Int, view: View)
fun onItemLongClick(item: I, position: Int, view: View) = false
}

View File

@@ -1,24 +0,0 @@
package org.koitharu.kotatsu.base.ui.list
import android.view.ViewGroup
class ProgressBarAdapter : BaseRecyclerAdapter<Boolean, Unit>() {
var isProgressVisible: Boolean
get() = dataSet.isNotEmpty()
set(value) {
if (value == dataSet.isEmpty()) {
if (value) {
appendItem(true)
} else {
removeItemAt(0)
}
}
}
override fun getExtra(item: Boolean, position: Int) = Unit
override fun onCreateViewHolder(parent: ViewGroup) = ProgressBarHolder(parent)
override fun onGetItemId(item: Boolean) = -1L
}

View File

@@ -14,7 +14,6 @@ import kotlinx.coroutines.withContext
import org.koin.android.viewmodel.ext.android.sharedViewModel
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.BaseFragment
import org.koitharu.kotatsu.core.model.FavouriteCategory
import org.koitharu.kotatsu.core.model.Manga
import org.koitharu.kotatsu.core.model.MangaHistory
import org.koitharu.kotatsu.favourites.ui.categories.select.FavouriteCategoriesDialog
@@ -105,12 +104,12 @@ class DetailsFragment : BaseFragment(R.layout.fragment_details), View.OnClickLis
}
}
private fun onFavouriteChanged(categories: List<FavouriteCategory>) {
private fun onFavouriteChanged(isFavourite: Boolean) {
imageView_favourite.setImageResource(
if (categories.isEmpty()) {
R.drawable.ic_heart_outline
} else {
if (isFavourite) {
R.drawable.ic_heart
} else {
R.drawable.ic_heart_outline
}
)
}

View File

@@ -39,8 +39,8 @@ class DetailsViewModel(
private val favourite = mangaData.mapNotNull { it?.id }
.distinctUntilChanged()
.flatMapLatest { mangaId ->
favouritesRepository.observeCategories(mangaId)
}.stateIn(viewModelScope, SharingStarted.Eagerly, emptyList())
favouritesRepository.observeCategoriesIds(mangaId).map { it.isNotEmpty() }
}.stateIn(viewModelScope, SharingStarted.Eagerly, false)
private val newChapters = mangaData.mapNotNull { it?.id }
.distinctUntilChanged()

View File

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

View File

@@ -2,8 +2,10 @@ package org.koitharu.kotatsu.favourites
import org.koin.android.viewmodel.dsl.viewModel
import org.koin.dsl.module
import org.koitharu.kotatsu.core.model.Manga
import org.koitharu.kotatsu.favourites.domain.FavouritesRepository
import org.koitharu.kotatsu.favourites.ui.categories.FavouritesCategoriesViewModel
import org.koitharu.kotatsu.favourites.ui.categories.select.MangaCategoriesViewModel
import org.koitharu.kotatsu.favourites.ui.list.FavouritesListViewModel
val favouritesModule
@@ -15,4 +17,7 @@ val favouritesModule
FavouritesListViewModel(categoryId, get(), get())
}
viewModel { FavouritesCategoriesViewModel(get()) }
viewModel { (manga: Manga) ->
MangaCategoriesViewModel(manga, get())
}
}

View File

@@ -42,6 +42,9 @@ abstract class FavouritesDao {
@Query("SELECT * FROM favourites WHERE manga_id = :id GROUP BY manga_id")
abstract fun observe(id: Long): Flow<FavouriteManga?>
@Query("SELECT DISTINCT category_id FROM favourites WHERE manga_id = :id")
abstract fun observeIds(id: Long): Flow<List<Long>>
@Insert(onConflict = OnConflictStrategy.IGNORE)
abstract suspend fun insert(favourite: FavouriteEntity)

View File

@@ -70,6 +70,10 @@ class FavouritesRepository(private val db: MangaDatabase) {
}
}
fun observeCategoriesIds(mangaId: Long): Flow<List<Long>> {
return db.favouritesDao.observeIds(mangaId)
}
suspend fun addCategory(title: String): FavouriteCategory {
val entity = FavouriteCategoryEntity(
title = title,

View File

@@ -2,7 +2,7 @@ package org.koitharu.kotatsu.favourites.ui.categories
import android.content.Context
import android.text.InputType
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import androidx.appcompat.app.AlertDialog
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.dialog.TextInputDialog
import org.koitharu.kotatsu.core.model.FavouriteCategory
@@ -13,7 +13,7 @@ class CategoriesEditDelegate(
) {
fun deleteCategory(category: FavouriteCategory) {
MaterialAlertDialogBuilder(context)
AlertDialog.Builder(context)
.setMessage(context.getString(R.string.category_delete_confirm, category.title))
.setTitle(R.string.remove_category)
.setNegativeButton(android.R.string.cancel, null)

View File

@@ -17,7 +17,7 @@ fun categoryAD(
@Suppress("ClickableViewAccessibility")
imageView_handle.setOnTouchListener { v, event ->
if (event.actionMasked == MotionEvent.ACTION_DOWN) {
clickListener.onItemLongClick(item, v)
clickListener.onItemLongClick(item, itemView)
} else {
false
}

View File

@@ -1,59 +1,42 @@
package org.koitharu.kotatsu.favourites.ui.categories
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.asLiveData
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.Dispatchers
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.core.model.Manga
import org.koitharu.kotatsu.favourites.domain.FavouritesRepository
import org.koitharu.kotatsu.utils.ext.mapToSet
class FavouritesCategoriesViewModel(
private val repository: FavouritesRepository
) : BaseViewModel() {
private var reorderJob: Job? = null
private var mangaSubscription: Job? = null
val categories = repository.observeCategories()
.asLiveData(viewModelScope.coroutineContext + Dispatchers.Default)
val mangaCategories = MutableLiveData<Set<Long>>(emptySet())
fun observeMangaCategories(mangaId: Long) {
mangaSubscription?.cancel()
mangaSubscription = repository.observeCategories(mangaId)
.map { list -> list.mapToSet { it.id } }
.onEach { mangaCategories.postValue(it) }
.launchIn(viewModelScope + Dispatchers.Default)
}
fun createCategory(name: String) {
launchJob {
launchJob(Dispatchers.Default) {
repository.addCategory(name)
}
}
fun renameCategory(id: Long, name: String) {
launchJob {
launchJob(Dispatchers.Default) {
repository.renameCategory(id, name)
}
}
fun deleteCategory(id: Long) {
launchJob {
launchJob(Dispatchers.Default) {
repository.removeCategory(id)
}
}
fun reorderCategories(oldPos: Int, newPos: Int) {
val prevJob = reorderJob
reorderJob = launchJob {
reorderJob = launchJob(Dispatchers.Default) {
prevJob?.join()
val items = categories.value ?: error("This should not happen")
val ids = items.mapTo(ArrayList(items.size)) { it.id }
@@ -62,16 +45,4 @@ class FavouritesCategoriesViewModel(
repository.reorderCategories(ids)
}
}
fun addToCategory(manga: Manga, categoryId: Long) {
launchJob {
repository.addToCategory(manga, categoryId)
}
}
fun removeFromCategory(manga: Manga, categoryId: Long) {
launchJob {
repository.removeFromCategory(manga, categoryId)
}
}
}

View File

@@ -1,45 +0,0 @@
package org.koitharu.kotatsu.favourites.ui.categories.select
import android.view.ViewGroup
import android.widget.Checkable
import androidx.collection.ArraySet
import org.koitharu.kotatsu.base.ui.list.BaseRecyclerAdapter
import org.koitharu.kotatsu.base.ui.list.BaseViewHolder
import org.koitharu.kotatsu.core.model.FavouriteCategory
class CategoriesSelectAdapter(private val listener: OnCategoryCheckListener) :
BaseRecyclerAdapter<FavouriteCategory, Boolean>() {
private val checkedIds = ArraySet<Long>()
fun setCheckedIds(ids: Iterable<Long>) {
checkedIds.clear()
checkedIds.addAll(ids)
notifyDataSetChanged()
}
override fun getExtra(item: FavouriteCategory, position: Int) = item.id in checkedIds
override fun onCreateViewHolder(parent: ViewGroup) =
CategoryCheckableHolder(
parent
)
override fun onGetItemId(item: FavouriteCategory) = item.id
override fun onViewDetachedFromWindow(holder: BaseViewHolder<FavouriteCategory, Boolean>) {
holder.itemView.setOnClickListener(null)
}
override fun onViewAttachedToWindow(holder: BaseViewHolder<FavouriteCategory, Boolean>) {
holder.itemView.setOnClickListener {
if (it !is Checkable) return@setOnClickListener
it.toggle()
if (it.isChecked) {
listener.onCategoryChecked(holder.requireData())
} else {
listener.onCategoryUnchecked(holder.requireData())
}
}
}
}

View File

@@ -1,16 +0,0 @@
package org.koitharu.kotatsu.favourites.ui.categories.select
import android.view.ViewGroup
import kotlinx.android.synthetic.main.item_category_checkable.*
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.list.BaseViewHolder
import org.koitharu.kotatsu.core.model.FavouriteCategory
class CategoryCheckableHolder(parent: ViewGroup) :
BaseViewHolder<FavouriteCategory, Boolean>(parent, R.layout.item_category_checkable) {
override fun onBind(data: FavouriteCategory, extra: Boolean) {
checkedTextView.text = data.title
checkedTextView.isChecked = extra
}
}

View File

@@ -1,46 +1,44 @@
package org.koitharu.kotatsu.favourites.ui.categories.select
import android.os.Bundle
import android.text.InputType
import android.view.View
import android.widget.Toast
import androidx.fragment.app.FragmentManager
import kotlinx.android.synthetic.main.dialog_favorite_categories.*
import org.koin.android.viewmodel.ext.android.viewModel
import org.koin.core.parameter.parametersOf
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.domain.MangaIntent
import org.koitharu.kotatsu.base.ui.BaseBottomSheet
import org.koitharu.kotatsu.base.ui.dialog.TextInputDialog
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.model.FavouriteCategory
import org.koitharu.kotatsu.core.model.Manga
import org.koitharu.kotatsu.favourites.ui.categories.FavouritesCategoriesViewModel
import org.koitharu.kotatsu.favourites.ui.categories.CategoriesEditDelegate
import org.koitharu.kotatsu.favourites.ui.categories.select.adapter.MangaCategoriesAdapter
import org.koitharu.kotatsu.favourites.ui.categories.select.model.MangaCategoryItem
import org.koitharu.kotatsu.utils.ext.getDisplayMessage
import org.koitharu.kotatsu.utils.ext.withArgs
class FavouriteCategoriesDialog : BaseBottomSheet(R.layout.dialog_favorite_categories),
OnCategoryCheckListener {
OnListItemClickListener<MangaCategoryItem>, CategoriesEditDelegate.CategoriesEditCallback,
View.OnClickListener {
private val viewModel by viewModel<FavouritesCategoriesViewModel>()
private val viewModel by viewModel<MangaCategoriesViewModel> {
parametersOf(requireNotNull(arguments?.getParcelable<Manga>(MangaIntent.KEY_MANGA)))
}
private val manga get() = arguments?.getParcelable<Manga>(ARG_MANGA)
private var adapter: CategoriesSelectAdapter? = null
private var adapter: MangaCategoriesAdapter? = null
private val editDelegate by lazy(LazyThreadSafetyMode.NONE) {
CategoriesEditDelegate(requireContext(), this@FavouriteCategoriesDialog)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
adapter =
CategoriesSelectAdapter(
this
)
adapter = MangaCategoriesAdapter(this)
recyclerView_categories.adapter = adapter
textView_add.setOnClickListener {
createCategory()
}
manga?.let {
viewModel.observeMangaCategories(it.id)
}
textView_add.setOnClickListener(this)
viewModel.categories.observe(viewLifecycleOwner, ::onCategoriesChanged)
viewModel.mangaCategories.observe(viewLifecycleOwner, ::onCheckedCategoriesChanged)
viewModel.content.observe(viewLifecycleOwner, this::onContentChanged)
viewModel.onError.observe(viewLifecycleOwner, ::onError)
}
@@ -49,50 +47,39 @@ class FavouriteCategoriesDialog : BaseBottomSheet(R.layout.dialog_favorite_categ
super.onDestroyView()
}
private fun onCategoriesChanged(categories: List<FavouriteCategory>) {
adapter?.replaceData(categories)
override fun onClick(v: View) {
when (v.id) {
R.id.textView_add -> editDelegate.createCategory()
}
}
private fun onCheckedCategoriesChanged(checkedIds: Set<Long>) {
adapter?.setCheckedIds(checkedIds)
override fun onItemClick(item: MangaCategoryItem, view: View) {
viewModel.setChecked(item.id, !item.isChecked)
}
override fun onCategoryChecked(category: FavouriteCategory) {
viewModel.addToCategory(manga ?: return, category.id)
override fun onDeleteCategory(category: FavouriteCategory) = Unit
override fun onRenameCategory(category: FavouriteCategory, newName: String) = Unit
override fun onCreateCategory(name: String) {
viewModel.createCategory(name)
}
override fun onCategoryUnchecked(category: FavouriteCategory) {
viewModel.removeFromCategory(manga ?: return, category.id)
private fun onContentChanged(categories: List<MangaCategoryItem>) {
adapter?.items = categories
}
private fun onError(e: Throwable) {
Toast.makeText(context ?: return, e.getDisplayMessage(resources), Toast.LENGTH_SHORT).show()
}
private fun createCategory() {
TextInputDialog.Builder(context ?: return)
.setTitle(R.string.add_new_category)
.setHint(R.string.enter_category_name)
.setMaxLength(12, false)
.setInputType(InputType.TYPE_TEXT_VARIATION_PERSON_NAME or InputType.TYPE_TEXT_FLAG_CAP_SENTENCES)
.setNegativeButton(android.R.string.cancel)
.setPositiveButton(R.string.add) { _, name ->
viewModel.createCategory(name)
}.create()
.show()
}
companion object {
private const val ARG_MANGA = "manga"
private const val TAG = "FavouriteCategoriesDialog"
fun show(fm: FragmentManager, manga: Manga) = FavouriteCategoriesDialog()
.withArgs(1) {
putParcelable(ARG_MANGA, manga)
}.show(
fm,
TAG
)
putParcelable(MangaIntent.KEY_MANGA, manga)
}.show(fm, TAG)
}
}

View File

@@ -0,0 +1,45 @@
package org.koitharu.kotatsu.favourites.ui.categories.select
import androidx.lifecycle.asLiveData
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.combine
import org.koitharu.kotatsu.base.ui.BaseViewModel
import org.koitharu.kotatsu.core.model.Manga
import org.koitharu.kotatsu.favourites.domain.FavouritesRepository
import org.koitharu.kotatsu.favourites.ui.categories.select.model.MangaCategoryItem
class MangaCategoriesViewModel(
private val manga: Manga,
private val favouritesRepository: FavouritesRepository
) : BaseViewModel() {
val content = combine(
favouritesRepository.observeCategories(),
favouritesRepository.observeCategoriesIds(manga.id)
) { all, checked ->
all.map {
MangaCategoryItem(
id = it.id,
name = it.title,
isChecked = it.id in checked
)
}
}.asLiveData(viewModelScope.coroutineContext + Dispatchers.Default)
fun setChecked(categoryId: Long, isChecked: Boolean) {
launchJob(Dispatchers.Default) {
if (isChecked) {
favouritesRepository.addToCategory(manga, categoryId)
} else {
favouritesRepository.removeFromCategory(manga, categoryId)
}
}
}
fun createCategory(name: String) {
launchJob(Dispatchers.Default) {
favouritesRepository.addCategory(name)
}
}
}

View File

@@ -1,10 +0,0 @@
package org.koitharu.kotatsu.favourites.ui.categories.select
import org.koitharu.kotatsu.core.model.FavouriteCategory
interface OnCategoryCheckListener {
fun onCategoryChecked(category: FavouriteCategory)
fun onCategoryUnchecked(category: FavouriteCategory)
}

View File

@@ -0,0 +1,23 @@
package org.koitharu.kotatsu.favourites.ui.categories.select.adapter
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateLayoutContainer
import kotlinx.android.synthetic.main.item_category_checkable.*
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.favourites.ui.categories.select.model.MangaCategoryItem
fun mangaCategoryAD(
clickListener: OnListItemClickListener<MangaCategoryItem>
) = adapterDelegateLayoutContainer<MangaCategoryItem, MangaCategoryItem>(
R.layout.item_category_checkable
) {
itemView.setOnClickListener {
clickListener.onItemClick(item, itemView)
}
bind {
checkedTextView.text = item.name
checkedTextView.isChecked = item.isChecked
}
}

View File

@@ -0,0 +1,37 @@
package org.koitharu.kotatsu.favourites.ui.categories.select.adapter
import androidx.recyclerview.widget.DiffUtil
import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.favourites.ui.categories.select.model.MangaCategoryItem
class MangaCategoriesAdapter(
clickListener: OnListItemClickListener<MangaCategoryItem>
) : AsyncListDifferDelegationAdapter<MangaCategoryItem>(DiffCallback()) {
init {
delegatesManager.addDelegate(mangaCategoryAD(clickListener))
}
private class DiffCallback : DiffUtil.ItemCallback<MangaCategoryItem>() {
override fun areItemsTheSame(
oldItem: MangaCategoryItem,
newItem: MangaCategoryItem
): Boolean = oldItem.id == newItem.id
override fun areContentsTheSame(
oldItem: MangaCategoryItem,
newItem: MangaCategoryItem
): Boolean = oldItem == newItem
override fun getChangePayload(
oldItem: MangaCategoryItem,
newItem: MangaCategoryItem
): Any? {
if (oldItem.isChecked != newItem.isChecked) {
return newItem.isChecked
}
return super.getChangePayload(oldItem, newItem)
}
}
}

View File

@@ -0,0 +1,7 @@
package org.koitharu.kotatsu.favourites.ui.categories.select.model
data class MangaCategoryItem(
val id: Long,
val name: String,
val isChecked: Boolean
)

View File

@@ -5,7 +5,7 @@ import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import androidx.appcompat.app.AlertDialog
import com.google.android.material.snackbar.Snackbar
import kotlinx.android.synthetic.main.fragment_list.*
import org.koin.android.viewmodel.ext.android.viewModel
@@ -34,7 +34,7 @@ class HistoryListFragment : MangaListFragment() {
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) {
R.id.action_clear_history -> {
MaterialAlertDialogBuilder(context ?: return false)
AlertDialog.Builder(context ?: return false)
.setTitle(R.string.clear_history)
.setMessage(R.string.text_clear_history_prompt)
.setNegativeButton(android.R.string.cancel, null)

View File

@@ -5,6 +5,7 @@ import org.koin.android.viewmodel.dsl.viewModel
import org.koin.core.qualifier.named
import org.koin.dsl.module
import org.koitharu.kotatsu.core.model.MangaSource
import org.koitharu.kotatsu.core.parser.MangaRepository
import org.koitharu.kotatsu.local.domain.LocalMangaRepository
import org.koitharu.kotatsu.local.ui.LocalListViewModel
@@ -12,7 +13,7 @@ val localModule
get() = module {
single { LocalMangaRepository(androidContext()) }
factory(named(MangaSource.LOCAL)) { get<LocalMangaRepository>() }
factory<MangaRepository>(named(MangaSource.LOCAL)) { get<LocalMangaRepository>() }
viewModel { LocalListViewModel(get(), get(), get(), androidContext()) }
}

View File

@@ -9,7 +9,7 @@ import android.view.MenuItem
import android.view.View
import androidx.activity.result.ActivityResultCallback
import androidx.activity.result.contract.ActivityResultContracts
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import androidx.appcompat.app.AlertDialog
import com.google.android.material.snackbar.Snackbar
import kotlinx.android.synthetic.main.fragment_list.*
import org.koin.android.viewmodel.ext.android.viewModel
@@ -86,7 +86,7 @@ class LocalListFragment : MangaListFragment(), ActivityResultCallback<Uri> {
override fun onPopupMenuItemSelected(item: MenuItem, data: Manga): Boolean {
return when (item.itemId) {
R.id.action_delete -> {
MaterialAlertDialogBuilder(context ?: return false)
AlertDialog.Builder(context ?: return false)
.setTitle(R.string.delete_manga)
.setMessage(getString(R.string.text_delete_local_manga, data.title))
.setPositiveButton(R.string.delete) { _, _ ->

View File

@@ -12,12 +12,12 @@ import android.view.*
import android.widget.Toast
import androidx.activity.result.ActivityResultCallback
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AlertDialog
import androidx.core.content.ContextCompat
import androidx.core.graphics.Insets
import androidx.core.view.*
import androidx.fragment.app.commit
import androidx.lifecycle.lifecycleScope
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.Snackbar
import kotlinx.android.synthetic.main.activity_reader.*
import kotlinx.coroutines.Dispatchers
@@ -247,7 +247,7 @@ class ReaderActivity : BaseFullscreenActivity(), ChaptersDialog.OnChapterChangeL
}
override fun onError(e: Throwable) {
val dialog = MaterialAlertDialogBuilder(this)
val dialog = AlertDialog.Builder(this)
.setTitle(R.string.error_occurred)
.setMessage(e.message)
.setPositiveButton(R.string.close, null)

View File

@@ -1,34 +0,0 @@
package org.koitharu.kotatsu.reader.ui.thumbnails
import android.view.ViewGroup
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.DisposableHandle
import kotlinx.coroutines.SupervisorJob
import org.koin.core.component.inject
import org.koitharu.kotatsu.base.ui.list.BaseRecyclerAdapter
import org.koitharu.kotatsu.base.ui.list.OnRecyclerItemClickListener
import org.koitharu.kotatsu.core.model.MangaPage
import org.koitharu.kotatsu.local.data.PagesCache
import kotlin.coroutines.CoroutineContext
class PagesThumbnailsAdapter(onItemClickListener: OnRecyclerItemClickListener<MangaPage>?) :
BaseRecyclerAdapter<MangaPage, PagesCache>(onItemClickListener), CoroutineScope,
DisposableHandle {
private val job = SupervisorJob()
private val cache by inject<PagesCache>()
override val coroutineContext: CoroutineContext
get() = Dispatchers.Main.immediate + job
override fun dispose() {
job.cancel()
}
override fun getExtra(item: MangaPage, position: Int) = cache
override fun onCreateViewHolder(parent: ViewGroup) = PageThumbnailHolder(parent, this)
override fun onGetItemId(item: MangaPage) = item.id
}

View File

@@ -8,30 +8,32 @@ import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialog
import kotlinx.android.synthetic.main.sheet_pages.*
import kotlinx.coroutines.DisposableHandle
import org.koin.android.ext.android.get
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.BaseBottomSheet
import org.koitharu.kotatsu.base.ui.list.OnRecyclerItemClickListener
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.base.ui.list.decor.SpacingItemDecoration
import org.koitharu.kotatsu.core.model.MangaPage
import org.koitharu.kotatsu.reader.ui.thumbnails.adapter.PageThumbnailAdapter
import org.koitharu.kotatsu.utils.UiUtils
import org.koitharu.kotatsu.utils.ext.resolveDp
import org.koitharu.kotatsu.utils.ext.viewLifecycleScope
import org.koitharu.kotatsu.utils.ext.withArgs
class PagesThumbnailsSheet : BaseBottomSheet(R.layout.sheet_pages),
OnRecyclerItemClickListener<MangaPage> {
OnListItemClickListener<MangaPage> {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
recyclerView.addItemDecoration(SpacingItemDecoration(view.resources.resolveDp(8)))
val pages = arguments?.getParcelableArrayList<MangaPage>(ARG_PAGES)
if (pages != null) {
recyclerView.adapter = PagesThumbnailsAdapter(this).apply {
replaceData(pages)
}
} else {
if (pages == null) {
dismissAllowingStateLoss()
return
}
recyclerView.adapter = PageThumbnailAdapter(get(), viewLifecycleScope, get(), this).apply {
items = pages
}
val title = arguments?.getString(ARG_TITLE)
toolbar.title = title
toolbar.setNavigationOnClickListener { dismiss() }
@@ -74,7 +76,7 @@ class PagesThumbnailsSheet : BaseBottomSheet(R.layout.sheet_pages),
super.onDestroyView()
}
override fun onItemClick(item: MangaPage, position: Int, view: View) {
override fun onItemClick(item: MangaPage, view: View) {
((parentFragment as? OnPageSelectListener)
?: (activity as? OnPageSelectListener))?.run {
onPageSelected(item)

View File

@@ -0,0 +1,59 @@
package org.koitharu.kotatsu.reader.ui.thumbnails.adapter
import androidx.core.net.toUri
import coil.ImageLoader
import coil.request.ImageRequest
import coil.size.PixelSize
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateLayoutContainer
import kotlinx.android.synthetic.main.item_page_thumb.*
import kotlinx.coroutines.*
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.model.MangaPage
import org.koitharu.kotatsu.local.data.PagesCache
import org.koitharu.kotatsu.utils.ext.IgnoreErrors
fun pageThumbnailAD(
coil: ImageLoader,
scope: CoroutineScope,
cache: PagesCache,
clickListener: OnListItemClickListener<MangaPage>
) = adapterDelegateLayoutContainer<MangaPage, MangaPage>(R.layout.item_page_thumb) {
var job: Job? = null
val gridWidth = itemView.context.resources.getDimensionPixelSize(R.dimen.preferred_grid_width)
val thumbSize = PixelSize(
width = gridWidth,
height = (gridWidth * 13f / 18f).toInt()
)
handle.setOnClickListener {
clickListener.onItemClick(item, itemView)
}
bind {
job?.cancel()
imageView_thumb.setImageDrawable(null)
textView_number.text = (bindingAdapterPosition + 1).toString()
job = scope.launch(Dispatchers.Default + IgnoreErrors) {
val url = item.preview ?: item.url.let {
val pageUrl = item.source.repository.getPageFullUrl(item)
cache[pageUrl]?.toUri()?.toString() ?: pageUrl
}
val drawable = coil.execute(
ImageRequest.Builder(context)
.data(url)
.size(thumbSize)
.build()
).drawable
withContext(Dispatchers.Main) {
imageView_thumb.setImageDrawable(drawable)
}
}
}
onViewRecycled {
job?.cancel()
imageView_thumb.setImageDrawable(null)
}
}

View File

@@ -0,0 +1,20 @@
package org.koitharu.kotatsu.reader.ui.thumbnails.adapter
import coil.ImageLoader
import com.hannesdorfmann.adapterdelegates4.ListDelegationAdapter
import kotlinx.coroutines.CoroutineScope
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.model.MangaPage
import org.koitharu.kotatsu.local.data.PagesCache
class PageThumbnailAdapter(
coil: ImageLoader,
scope: CoroutineScope,
cache: PagesCache,
clickListener: OnListItemClickListener<MangaPage>
) : ListDelegationAdapter<List<MangaPage>>() {
init {
delegatesManager.addDelegate(pageThumbnailAD(coil, scope, cache, clickListener))
}
}

View File

@@ -6,8 +6,8 @@ import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
import androidx.activity.ComponentActivity
import androidx.appcompat.app.AlertDialog
import androidx.lifecycle.lifecycleScope
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
@@ -68,7 +68,7 @@ class AppUpdateChecker(private val activity: ComponentActivity) {
}
private fun showUpdateDialog(version: AppVersion) {
MaterialAlertDialogBuilder(activity)
AlertDialog.Builder(activity)
.setTitle(R.string.app_update_available)
.setMessage(buildString {
append(activity.getString(R.string.new_version_s, version.name))

View File

@@ -5,20 +5,19 @@ import android.view.MotionEvent
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import kotlinx.android.synthetic.main.item_source_config.*
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
import org.koitharu.kotatsu.base.domain.MangaProviderFactory
import org.koitharu.kotatsu.base.ui.list.OnRecyclerItemClickListener
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.model.MangaSource
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.utils.ext.mapToSet
import org.koitharu.kotatsu.utils.ext.safe
class SourcesAdapter(private val onItemClickListener: OnRecyclerItemClickListener<MangaSource>) :
RecyclerView.Adapter<SourceViewHolder>(), KoinComponent {
class SourcesAdapter(
private val settings: AppSettings,
private val onItemClickListener: OnListItemClickListener<MangaSource>,
) : RecyclerView.Adapter<SourceViewHolder>() {
private val dataSet = MangaProviderFactory.getSources(includeHidden = true).toMutableList()
private val settings by inject<AppSettings>()
private val hiddenItems = settings.hiddenSources.mapNotNull {
safe {
MangaSource.valueOf(it)
@@ -48,14 +47,13 @@ class SourcesAdapter(private val onItemClickListener: OnRecyclerItemClickListene
settings.hiddenSources = hiddenItems.mapToSet { x -> x.name }
}
holder.imageView_config.setOnClickListener { v ->
onItemClickListener.onItemClick(holder.requireData(), holder.bindingAdapterPosition, v)
onItemClickListener.onItemClick(holder.requireData(), v)
}
holder.imageView_handle.setOnTouchListener { v, event ->
if (event.actionMasked == MotionEvent.ACTION_DOWN) {
onItemClickListener.onItemLongClick(
holder.requireData(),
holder.bindingAdapterPosition,
v
holder.itemView
)
} else {
false

View File

@@ -6,14 +6,15 @@ import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.RecyclerView
import kotlinx.android.synthetic.main.fragment_settings_sources.*
import org.koin.android.ext.android.get
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.BaseFragment
import org.koitharu.kotatsu.base.ui.list.OnRecyclerItemClickListener
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.model.MangaSource
import org.koitharu.kotatsu.settings.SettingsActivity
class SourcesSettingsFragment : BaseFragment(R.layout.fragment_settings_sources),
OnRecyclerItemClickListener<MangaSource> {
OnListItemClickListener<MangaSource> {
private lateinit var reorderHelper: ItemTouchHelper
@@ -30,7 +31,7 @@ class SourcesSettingsFragment : BaseFragment(R.layout.fragment_settings_sources)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
recyclerView.addItemDecoration(DividerItemDecoration(view.context, RecyclerView.VERTICAL))
recyclerView.adapter = SourcesAdapter(this)
recyclerView.adapter = SourcesAdapter(get(), this)
reorderHelper.attachToRecyclerView(recyclerView)
}
@@ -39,13 +40,13 @@ class SourcesSettingsFragment : BaseFragment(R.layout.fragment_settings_sources)
super.onDestroyView()
}
override fun onItemClick(item: MangaSource, position: Int, view: View) {
override fun onItemClick(item: MangaSource, view: View) {
(activity as? SettingsActivity)?.openMangaSourceSettings(item)
}
override fun onItemLongClick(item: MangaSource, position: Int, view: View): Boolean {
override fun onItemLongClick(item: MangaSource, view: View): Boolean {
reorderHelper.startDrag(
recyclerView.findViewHolderForAdapterPosition(position) ?: return false
recyclerView.findContainingViewHolder(view) ?: return false
)
return true
}

View File

@@ -43,15 +43,14 @@ class FeedViewModel(
if (loadingJob?.isActive == true) {
return
}
loadingJob = launchLoadingJob {
loadingJob = launchLoadingJob(Dispatchers.Default) {
val offset = if (append) logList.value.size else 0
val list = repository.getTrackingLog(offset, 20)
if (!append) {
logList.value = list
isEmptyState.postValue(list.isEmpty())
} else if (list.isNotEmpty()) {
logList.value += list
} else {
isEmptyState.value = true
}
hasNextPage.value = list.isNotEmpty()
}

View File

@@ -0,0 +1,10 @@
package org.koitharu.kotatsu.widget
import org.koin.android.viewmodel.dsl.viewModel
import org.koin.dsl.module
import org.koitharu.kotatsu.widget.shelf.ShelfConfigViewModel
val appWidgetModule
get() = module {
viewModel { ShelfConfigViewModel(get()) }
}

View File

@@ -1,35 +0,0 @@
package org.koitharu.kotatsu.widget.shelf
import android.view.ViewGroup
import org.koitharu.kotatsu.base.ui.list.BaseRecyclerAdapter
import org.koitharu.kotatsu.base.ui.list.OnRecyclerItemClickListener
import org.koitharu.kotatsu.core.model.FavouriteCategory
class CategorySelectAdapter(onItemClickListener: OnRecyclerItemClickListener<FavouriteCategory>? = null) :
BaseRecyclerAdapter<FavouriteCategory, Boolean>(onItemClickListener) {
var checkedItemId = 0L
private set
fun setCheckedId(id: Long) {
val oldId = checkedItemId
checkedItemId = id
val oldPos = findItemPositionById(oldId)
val newPos = findItemPositionById(id)
if (newPos != -1) {
notifyItemChanged(newPos)
}
if (oldPos != -1) {
notifyItemChanged(oldPos)
}
}
override fun getExtra(item: FavouriteCategory, position: Int) =
checkedItemId == item.id
override fun onCreateViewHolder(parent: ViewGroup) = CategorySelectHolder(
parent
)
override fun onGetItemId(item: FavouriteCategory) = item.id
}

View File

@@ -1,16 +0,0 @@
package org.koitharu.kotatsu.widget.shelf
import android.view.ViewGroup
import kotlinx.android.synthetic.main.item_category_checkable.*
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.list.BaseViewHolder
import org.koitharu.kotatsu.core.model.FavouriteCategory
class CategorySelectHolder(parent: ViewGroup) :
BaseViewHolder<FavouriteCategory, Boolean>(parent, R.layout.item_category_checkable_single) {
override fun onBind(data: FavouriteCategory, extra: Boolean) {
checkedTextView.text = data.title
checkedTextView.isChecked = extra
}
}

View File

@@ -17,17 +17,15 @@ import kotlinx.android.synthetic.main.activity_categories.*
import org.koin.android.viewmodel.ext.android.viewModel
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.BaseActivity
import org.koitharu.kotatsu.base.ui.list.OnRecyclerItemClickListener
import org.koitharu.kotatsu.core.model.FavouriteCategory
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.prefs.AppWidgetConfig
import org.koitharu.kotatsu.favourites.ui.categories.FavouritesCategoriesViewModel
import org.koitharu.kotatsu.utils.ext.getDisplayMessage
import java.util.*
import kotlin.collections.ArrayList
import org.koitharu.kotatsu.widget.shelf.adapter.CategorySelectAdapter
import org.koitharu.kotatsu.widget.shelf.model.CategoryItem
class ShelfConfigActivity : BaseActivity(), OnRecyclerItemClickListener<FavouriteCategory> {
class ShelfConfigActivity : BaseActivity(), OnListItemClickListener<CategoryItem> {
private val viewModel by viewModel<FavouritesCategoriesViewModel>()
private val viewModel by viewModel<ShelfConfigViewModel>()
private lateinit var adapter: CategorySelectAdapter
private lateinit var config: AppWidgetConfig
@@ -50,10 +48,10 @@ class ShelfConfigActivity : BaseActivity(), OnRecyclerItemClickListener<Favourit
return
}
config = AppWidgetConfig.getInstance(this, appWidgetId)
adapter.setCheckedId(config.categoryId)
viewModel.checkedId = config.categoryId
viewModel.categories.observe(this, ::onCategoriesChanged)
viewModel.onError.observe(this, ::onError)
viewModel.content.observe(this, this::onContentChanged)
viewModel.onError.observe(this, this::onError)
}
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
@@ -63,7 +61,7 @@ class ShelfConfigActivity : BaseActivity(), OnRecyclerItemClickListener<Favourit
override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) {
R.id.action_done -> {
config.categoryId = adapter.checkedItemId
config.categoryId = viewModel.checkedId
updateWidget()
setResult(
Activity.RESULT_OK,
@@ -75,15 +73,12 @@ class ShelfConfigActivity : BaseActivity(), OnRecyclerItemClickListener<Favourit
else -> super.onOptionsItemSelected(item)
}
override fun onItemClick(item: FavouriteCategory, position: Int, view: View) {
adapter.setCheckedId(item.id)
override fun onItemClick(item: CategoryItem, view: View) {
viewModel.checkedId = item.id
}
private fun onCategoriesChanged(categories: List<FavouriteCategory>) {
val data = ArrayList<FavouriteCategory>(categories.size + 1)
data += FavouriteCategory(0L, getString(R.string.all_favourites), -1, Date())
data += categories
adapter.replaceData(data)
private fun onContentChanged(categories: List<CategoryItem>) {
adapter.items = categories
}
private fun onError(e: Throwable) {

View File

@@ -0,0 +1,32 @@
package org.koitharu.kotatsu.widget.shelf
import androidx.lifecycle.asLiveData
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.combine
import org.koitharu.kotatsu.base.ui.BaseViewModel
import org.koitharu.kotatsu.favourites.domain.FavouritesRepository
import org.koitharu.kotatsu.widget.shelf.model.CategoryItem
import java.util.*
class ShelfConfigViewModel(
favouritesRepository: FavouritesRepository
) : BaseViewModel() {
private val selectedCategoryId = MutableStateFlow(0L)
val content = combine(
favouritesRepository.observeCategories(),
selectedCategoryId
) { categories, selectedId ->
val list = ArrayList<CategoryItem>(categories.size + 1)
list += CategoryItem(0L, null, selectedId == 0L)
categories.mapTo(list) {
CategoryItem(it.id, it.title, selectedId == it.id)
}
list
}.asLiveData(viewModelScope.coroutineContext + Dispatchers.Default)
var checkedId: Long by selectedCategoryId::value
}

View File

@@ -0,0 +1,33 @@
package org.koitharu.kotatsu.widget.shelf.adapter
import androidx.recyclerview.widget.DiffUtil
import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.widget.shelf.model.CategoryItem
class CategorySelectAdapter(
clickListener: OnListItemClickListener<CategoryItem>
) : AsyncListDifferDelegationAdapter<CategoryItem>(DiffCallback()) {
init {
delegatesManager.addDelegate(categorySelectItemAD(clickListener))
}
private class DiffCallback : DiffUtil.ItemCallback<CategoryItem>() {
override fun areItemsTheSame(oldItem: CategoryItem, newItem: CategoryItem): Boolean {
return oldItem.id == newItem.id
}
override fun areContentsTheSame(oldItem: CategoryItem, newItem: CategoryItem): Boolean {
return oldItem == newItem
}
override fun getChangePayload(oldItem: CategoryItem, newItem: CategoryItem): Any? {
if (oldItem.isSelected != newItem.isSelected) {
return newItem.isSelected
}
return super.getChangePayload(oldItem, newItem)
}
}
}

View File

@@ -0,0 +1,23 @@
package org.koitharu.kotatsu.widget.shelf.adapter
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateLayoutContainer
import kotlinx.android.synthetic.main.item_category_checkable.*
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.widget.shelf.model.CategoryItem
fun categorySelectItemAD(
clickListener: OnListItemClickListener<CategoryItem>
) = adapterDelegateLayoutContainer<CategoryItem, CategoryItem>(
R.layout.item_category_checkable_single
) {
itemView.setOnClickListener {
clickListener.onItemClick(item, it)
}
bind {
checkedTextView.text = item.name ?: getString(R.string.all_favourites)
checkedTextView.isChecked = item.isSelected
}
}

View File

@@ -0,0 +1,7 @@
package org.koitharu.kotatsu.widget.shelf.model
data class CategoryItem(
val id: Long,
val name: String?,
val isSelected: Boolean
)