Fully migrate to AdapterDelegates and cleanup code
This commit is contained in:
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
@@ -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)
|
||||
|
||||
@@ -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()) }
|
||||
}
|
||||
@@ -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) { _, _ ->
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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()) }
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package org.koitharu.kotatsu.widget.shelf.model
|
||||
|
||||
data class CategoryItem(
|
||||
val id: Long,
|
||||
val name: String?,
|
||||
val isSelected: Boolean
|
||||
)
|
||||
Reference in New Issue
Block a user