Fully migrate to AdapterDelegates and cleanup code

pull/26/head
Koitharu 5 years ago
parent 53e36d23b1
commit 5ed4d0b6b7

@ -1,15 +1,9 @@
language: android language: android
dist: trusty dist: trusty
jdk:
- oraclejdk8
android: android:
components: components:
- tools
- platform-tools-30.0.3
- build-tools-30.0.2
- android-30 - android-30
licenses: - build-tools-30.0.2
- android-sdk-preview-license-.+ - platform-tools-30.0.3
- android-sdk-license-.+ - tools
- google-gdk-license-.+
script: ./gradlew -Dorg.gradle.jvmargs=-Xmx1536m assembleDebug lintDebug script: ./gradlew -Dorg.gradle.jvmargs=-Xmx1536m assembleDebug lintDebug

@ -95,7 +95,7 @@ dependencies {
implementation 'org.koin:koin-android:2.2.0' implementation 'org.koin:koin-android:2.2.0'
implementation 'org.koin:koin-android-viewmodel:2.2.0' implementation 'org.koin:koin-android-viewmodel:2.2.0'
implementation 'io.coil-kt:coil-base:1.0.0' implementation 'io.coil-kt:coil-base:1.1.0'
implementation 'com.davemorrissey.labs:subsampling-scale-image-view:3.10.0' implementation 'com.davemorrissey.labs:subsampling-scale-image-view:3.10.0'
implementation 'com.tomclaw.cache:cache:1.0' implementation 'com.tomclaw.cache:cache:1.0'

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

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

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

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

@ -4,6 +4,7 @@ import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import java.util.* import java.util.*
@Deprecated("")
class AdapterUpdater<T>(oldList: List<T>, newList: List<T>, getId: (T) -> Long) { class AdapterUpdater<T>(oldList: List<T>, newList: List<T>, getId: (T) -> Long) {
private val diff = DiffUtil.calculateDiff(object : DiffUtil.Callback() { 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()") 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 open fun onRecycled() = Unit
abstract fun onBind(data: T, extra: E) 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.koin.android.viewmodel.ext.android.sharedViewModel
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.BaseFragment import org.koitharu.kotatsu.base.ui.BaseFragment
import org.koitharu.kotatsu.core.model.FavouriteCategory
import org.koitharu.kotatsu.core.model.Manga import org.koitharu.kotatsu.core.model.Manga
import org.koitharu.kotatsu.core.model.MangaHistory import org.koitharu.kotatsu.core.model.MangaHistory
import org.koitharu.kotatsu.favourites.ui.categories.select.FavouriteCategoriesDialog 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( imageView_favourite.setImageResource(
if (categories.isEmpty()) { if (isFavourite) {
R.drawable.ic_heart_outline
} else {
R.drawable.ic_heart R.drawable.ic_heart
} else {
R.drawable.ic_heart_outline
} }
) )
} }

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

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

@ -2,8 +2,10 @@ package org.koitharu.kotatsu.favourites
import org.koin.android.viewmodel.dsl.viewModel import org.koin.android.viewmodel.dsl.viewModel
import org.koin.dsl.module import org.koin.dsl.module
import org.koitharu.kotatsu.core.model.Manga
import org.koitharu.kotatsu.favourites.domain.FavouritesRepository import org.koitharu.kotatsu.favourites.domain.FavouritesRepository
import org.koitharu.kotatsu.favourites.ui.categories.FavouritesCategoriesViewModel 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 import org.koitharu.kotatsu.favourites.ui.list.FavouritesListViewModel
val favouritesModule val favouritesModule
@ -15,4 +17,7 @@ val favouritesModule
FavouritesListViewModel(categoryId, get(), get()) FavouritesListViewModel(categoryId, get(), get())
} }
viewModel { FavouritesCategoriesViewModel(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") @Query("SELECT * FROM favourites WHERE manga_id = :id GROUP BY manga_id")
abstract fun observe(id: Long): Flow<FavouriteManga?> 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) @Insert(onConflict = OnConflictStrategy.IGNORE)
abstract suspend fun insert(favourite: FavouriteEntity) 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 { suspend fun addCategory(title: String): FavouriteCategory {
val entity = FavouriteCategoryEntity( val entity = FavouriteCategoryEntity(
title = title, title = title,

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

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

@ -1,59 +1,42 @@
package org.koitharu.kotatsu.favourites.ui.categories package org.koitharu.kotatsu.favourites.ui.categories
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.asLiveData import androidx.lifecycle.asLiveData
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.plus
import org.koitharu.kotatsu.base.ui.BaseViewModel import org.koitharu.kotatsu.base.ui.BaseViewModel
import org.koitharu.kotatsu.core.model.Manga
import org.koitharu.kotatsu.favourites.domain.FavouritesRepository import org.koitharu.kotatsu.favourites.domain.FavouritesRepository
import org.koitharu.kotatsu.utils.ext.mapToSet
class FavouritesCategoriesViewModel( class FavouritesCategoriesViewModel(
private val repository: FavouritesRepository private val repository: FavouritesRepository
) : BaseViewModel() { ) : BaseViewModel() {
private var reorderJob: Job? = null private var reorderJob: Job? = null
private var mangaSubscription: Job? = null
val categories = repository.observeCategories() val categories = repository.observeCategories()
.asLiveData(viewModelScope.coroutineContext + Dispatchers.Default) .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) { fun createCategory(name: String) {
launchJob { launchJob(Dispatchers.Default) {
repository.addCategory(name) repository.addCategory(name)
} }
} }
fun renameCategory(id: Long, name: String) { fun renameCategory(id: Long, name: String) {
launchJob { launchJob(Dispatchers.Default) {
repository.renameCategory(id, name) repository.renameCategory(id, name)
} }
} }
fun deleteCategory(id: Long) { fun deleteCategory(id: Long) {
launchJob { launchJob(Dispatchers.Default) {
repository.removeCategory(id) repository.removeCategory(id)
} }
} }
fun reorderCategories(oldPos: Int, newPos: Int) { fun reorderCategories(oldPos: Int, newPos: Int) {
val prevJob = reorderJob val prevJob = reorderJob
reorderJob = launchJob { reorderJob = launchJob(Dispatchers.Default) {
prevJob?.join() prevJob?.join()
val items = categories.value ?: error("This should not happen") val items = categories.value ?: error("This should not happen")
val ids = items.mapTo(ArrayList(items.size)) { it.id } val ids = items.mapTo(ArrayList(items.size)) { it.id }
@ -62,16 +45,4 @@ class FavouritesCategoriesViewModel(
repository.reorderCategories(ids) 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 package org.koitharu.kotatsu.favourites.ui.categories.select
import android.os.Bundle import android.os.Bundle
import android.text.InputType
import android.view.View import android.view.View
import android.widget.Toast import android.widget.Toast
import androidx.fragment.app.FragmentManager import androidx.fragment.app.FragmentManager
import kotlinx.android.synthetic.main.dialog_favorite_categories.* import kotlinx.android.synthetic.main.dialog_favorite_categories.*
import org.koin.android.viewmodel.ext.android.viewModel import org.koin.android.viewmodel.ext.android.viewModel
import org.koin.core.parameter.parametersOf
import org.koitharu.kotatsu.R 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.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.FavouriteCategory
import org.koitharu.kotatsu.core.model.Manga 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.getDisplayMessage
import org.koitharu.kotatsu.utils.ext.withArgs import org.koitharu.kotatsu.utils.ext.withArgs
class FavouriteCategoriesDialog : BaseBottomSheet(R.layout.dialog_favorite_categories), 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?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
adapter = adapter = MangaCategoriesAdapter(this)
CategoriesSelectAdapter(
this
)
recyclerView_categories.adapter = adapter recyclerView_categories.adapter = adapter
textView_add.setOnClickListener { textView_add.setOnClickListener(this)
createCategory()
}
manga?.let {
viewModel.observeMangaCategories(it.id)
}
viewModel.categories.observe(viewLifecycleOwner, ::onCategoriesChanged) viewModel.content.observe(viewLifecycleOwner, this::onContentChanged)
viewModel.mangaCategories.observe(viewLifecycleOwner, ::onCheckedCategoriesChanged)
viewModel.onError.observe(viewLifecycleOwner, ::onError) viewModel.onError.observe(viewLifecycleOwner, ::onError)
} }
@ -49,50 +47,39 @@ class FavouriteCategoriesDialog : BaseBottomSheet(R.layout.dialog_favorite_categ
super.onDestroyView() super.onDestroyView()
} }
private fun onCategoriesChanged(categories: List<FavouriteCategory>) { override fun onClick(v: View) {
adapter?.replaceData(categories) when (v.id) {
R.id.textView_add -> editDelegate.createCategory()
}
} }
private fun onCheckedCategoriesChanged(checkedIds: Set<Long>) { override fun onItemClick(item: MangaCategoryItem, view: View) {
adapter?.setCheckedIds(checkedIds) viewModel.setChecked(item.id, !item.isChecked)
} }
override fun onCategoryChecked(category: FavouriteCategory) { override fun onDeleteCategory(category: FavouriteCategory) = Unit
viewModel.addToCategory(manga ?: return, category.id)
override fun onRenameCategory(category: FavouriteCategory, newName: String) = Unit
override fun onCreateCategory(name: String) {
viewModel.createCategory(name)
} }
override fun onCategoryUnchecked(category: FavouriteCategory) { private fun onContentChanged(categories: List<MangaCategoryItem>) {
viewModel.removeFromCategory(manga ?: return, category.id) adapter?.items = categories
} }
private fun onError(e: Throwable) { private fun onError(e: Throwable) {
Toast.makeText(context ?: return, e.getDisplayMessage(resources), Toast.LENGTH_SHORT).show() 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 { companion object {
private const val ARG_MANGA = "manga"
private const val TAG = "FavouriteCategoriesDialog" private const val TAG = "FavouriteCategoriesDialog"
fun show(fm: FragmentManager, manga: Manga) = FavouriteCategoriesDialog() fun show(fm: FragmentManager, manga: Manga) = FavouriteCategoriesDialog()
.withArgs(1) { .withArgs(1) {
putParcelable(ARG_MANGA, manga) putParcelable(MangaIntent.KEY_MANGA, manga)
}.show( }.show(fm, TAG)
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.MenuInflater
import android.view.MenuItem import android.view.MenuItem
import android.view.View import android.view.View
import com.google.android.material.dialog.MaterialAlertDialogBuilder import androidx.appcompat.app.AlertDialog
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import kotlinx.android.synthetic.main.fragment_list.* import kotlinx.android.synthetic.main.fragment_list.*
import org.koin.android.viewmodel.ext.android.viewModel import org.koin.android.viewmodel.ext.android.viewModel
@ -34,7 +34,7 @@ class HistoryListFragment : MangaListFragment() {
override fun onOptionsItemSelected(item: MenuItem): Boolean { override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) { return when (item.itemId) {
R.id.action_clear_history -> { R.id.action_clear_history -> {
MaterialAlertDialogBuilder(context ?: return false) AlertDialog.Builder(context ?: return false)
.setTitle(R.string.clear_history) .setTitle(R.string.clear_history)
.setMessage(R.string.text_clear_history_prompt) .setMessage(R.string.text_clear_history_prompt)
.setNegativeButton(android.R.string.cancel, null) .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.core.qualifier.named
import org.koin.dsl.module import org.koin.dsl.module
import org.koitharu.kotatsu.core.model.MangaSource 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.domain.LocalMangaRepository
import org.koitharu.kotatsu.local.ui.LocalListViewModel import org.koitharu.kotatsu.local.ui.LocalListViewModel
@ -12,7 +13,7 @@ val localModule
get() = module { get() = module {
single { LocalMangaRepository(androidContext()) } single { LocalMangaRepository(androidContext()) }
factory(named(MangaSource.LOCAL)) { get<LocalMangaRepository>() } factory<MangaRepository>(named(MangaSource.LOCAL)) { get<LocalMangaRepository>() }
viewModel { LocalListViewModel(get(), get(), get(), androidContext()) } viewModel { LocalListViewModel(get(), get(), get(), androidContext()) }
} }

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

@ -12,12 +12,12 @@ import android.view.*
import android.widget.Toast import android.widget.Toast
import androidx.activity.result.ActivityResultCallback import androidx.activity.result.ActivityResultCallback
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AlertDialog
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.graphics.Insets import androidx.core.graphics.Insets
import androidx.core.view.* import androidx.core.view.*
import androidx.fragment.app.commit import androidx.fragment.app.commit
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import kotlinx.android.synthetic.main.activity_reader.* import kotlinx.android.synthetic.main.activity_reader.*
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@ -247,7 +247,7 @@ class ReaderActivity : BaseFullscreenActivity(), ChaptersDialog.OnChapterChangeL
} }
override fun onError(e: Throwable) { override fun onError(e: Throwable) {
val dialog = MaterialAlertDialogBuilder(this) val dialog = AlertDialog.Builder(this)
.setTitle(R.string.error_occurred) .setTitle(R.string.error_occurred)
.setMessage(e.message) .setMessage(e.message)
.setPositiveButton(R.string.close, null) .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 com.google.android.material.bottomsheet.BottomSheetDialog
import kotlinx.android.synthetic.main.sheet_pages.* import kotlinx.android.synthetic.main.sheet_pages.*
import kotlinx.coroutines.DisposableHandle import kotlinx.coroutines.DisposableHandle
import org.koin.android.ext.android.get
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.BaseBottomSheet 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.base.ui.list.decor.SpacingItemDecoration
import org.koitharu.kotatsu.core.model.MangaPage 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.UiUtils
import org.koitharu.kotatsu.utils.ext.resolveDp import org.koitharu.kotatsu.utils.ext.resolveDp
import org.koitharu.kotatsu.utils.ext.viewLifecycleScope
import org.koitharu.kotatsu.utils.ext.withArgs import org.koitharu.kotatsu.utils.ext.withArgs
class PagesThumbnailsSheet : BaseBottomSheet(R.layout.sheet_pages), class PagesThumbnailsSheet : BaseBottomSheet(R.layout.sheet_pages),
OnRecyclerItemClickListener<MangaPage> { OnListItemClickListener<MangaPage> {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
recyclerView.addItemDecoration(SpacingItemDecoration(view.resources.resolveDp(8))) recyclerView.addItemDecoration(SpacingItemDecoration(view.resources.resolveDp(8)))
val pages = arguments?.getParcelableArrayList<MangaPage>(ARG_PAGES) val pages = arguments?.getParcelableArrayList<MangaPage>(ARG_PAGES)
if (pages != null) { if (pages == null) {
recyclerView.adapter = PagesThumbnailsAdapter(this).apply {
replaceData(pages)
}
} else {
dismissAllowingStateLoss() dismissAllowingStateLoss()
return return
} }
recyclerView.adapter = PageThumbnailAdapter(get(), viewLifecycleScope, get(), this).apply {
items = pages
}
val title = arguments?.getString(ARG_TITLE) val title = arguments?.getString(ARG_TITLE)
toolbar.title = title toolbar.title = title
toolbar.setNavigationOnClickListener { dismiss() } toolbar.setNavigationOnClickListener { dismiss() }
@ -74,7 +76,7 @@ class PagesThumbnailsSheet : BaseBottomSheet(R.layout.sheet_pages),
super.onDestroyView() super.onDestroyView()
} }
override fun onItemClick(item: MangaPage, position: Int, view: View) { override fun onItemClick(item: MangaPage, view: View) {
((parentFragment as? OnPageSelectListener) ((parentFragment as? OnPageSelectListener)
?: (activity as? OnPageSelectListener))?.run { ?: (activity as? OnPageSelectListener))?.run {
onPageSelected(item) 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.content.pm.PackageManager
import android.net.Uri import android.net.Uri
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.appcompat.app.AlertDialog
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -68,7 +68,7 @@ class AppUpdateChecker(private val activity: ComponentActivity) {
} }
private fun showUpdateDialog(version: AppVersion) { private fun showUpdateDialog(version: AppVersion) {
MaterialAlertDialogBuilder(activity) AlertDialog.Builder(activity)
.setTitle(R.string.app_update_available) .setTitle(R.string.app_update_available)
.setMessage(buildString { .setMessage(buildString {
append(activity.getString(R.string.new_version_s, version.name)) append(activity.getString(R.string.new_version_s, version.name))

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

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

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

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