Merge branch 'devel' into feature/kitsu
# Conflicts: # app/src/main/res/values/strings.xmlfeature/kitsu
commit
cb64740349
@ -0,0 +1,11 @@
|
|||||||
|
package org.koitharu.kotatsu.core.db.migrations
|
||||||
|
|
||||||
|
import androidx.room.migration.Migration
|
||||||
|
import androidx.sqlite.db.SupportSQLiteDatabase
|
||||||
|
|
||||||
|
class Migration15To16 : Migration(15, 16) {
|
||||||
|
|
||||||
|
override fun migrate(database: SupportSQLiteDatabase) {
|
||||||
|
database.execSQL("ALTER TABLE preferences ADD COLUMN `cf_invert` INTEGER NOT NULL DEFAULT 0")
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,103 @@
|
|||||||
|
package org.koitharu.kotatsu.core.network
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.collection.ArraySet
|
||||||
|
import coil.intercept.Interceptor
|
||||||
|
import coil.request.ErrorResult
|
||||||
|
import coil.request.ImageResult
|
||||||
|
import coil.request.SuccessResult
|
||||||
|
import coil.size.Dimension
|
||||||
|
import okhttp3.HttpUrl
|
||||||
|
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
|
import okhttp3.Request
|
||||||
|
import okhttp3.Response
|
||||||
|
import org.koitharu.kotatsu.BuildConfig
|
||||||
|
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||||
|
import org.koitharu.kotatsu.core.util.ext.ensureSuccess
|
||||||
|
import org.koitharu.kotatsu.core.util.ext.isHttpOrHttps
|
||||||
|
import org.koitharu.kotatsu.parsers.util.await
|
||||||
|
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
|
||||||
|
import java.util.Collections
|
||||||
|
import javax.inject.Inject
|
||||||
|
import javax.inject.Singleton
|
||||||
|
|
||||||
|
@Singleton
|
||||||
|
class ImageProxyInterceptor @Inject constructor(
|
||||||
|
private val settings: AppSettings,
|
||||||
|
) : Interceptor {
|
||||||
|
|
||||||
|
private val blacklist = Collections.synchronizedSet(ArraySet<String>())
|
||||||
|
|
||||||
|
override suspend fun intercept(chain: Interceptor.Chain): ImageResult {
|
||||||
|
val request = chain.request
|
||||||
|
if (!settings.isImagesProxyEnabled) {
|
||||||
|
return chain.proceed(request)
|
||||||
|
}
|
||||||
|
val url: HttpUrl? = when (val data = request.data) {
|
||||||
|
is HttpUrl -> data
|
||||||
|
is String -> data.toHttpUrlOrNull()
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
if (url == null || !url.isHttpOrHttps || url.host in blacklist) {
|
||||||
|
return chain.proceed(request)
|
||||||
|
}
|
||||||
|
val newUrl = HttpUrl.Builder()
|
||||||
|
.scheme("https")
|
||||||
|
.host("wsrv.nl")
|
||||||
|
.addQueryParameter("url", url.toString())
|
||||||
|
.addQueryParameter("fit", "outside")
|
||||||
|
.addQueryParameter("we", null)
|
||||||
|
val size = request.sizeResolver.size()
|
||||||
|
(size.height as? Dimension.Pixels)?.let { newUrl.addQueryParameter("h", it.toString()) }
|
||||||
|
(size.width as? Dimension.Pixels)?.let { newUrl.addQueryParameter("w", it.toString()) }
|
||||||
|
|
||||||
|
val newRequest = request.newBuilder()
|
||||||
|
.data(newUrl.build())
|
||||||
|
.build()
|
||||||
|
val result = chain.proceed(newRequest)
|
||||||
|
return if (result is SuccessResult) {
|
||||||
|
result
|
||||||
|
} else {
|
||||||
|
logDebug((result as? ErrorResult)?.throwable)
|
||||||
|
chain.proceed(request).also {
|
||||||
|
if (it is SuccessResult) {
|
||||||
|
blacklist.add(url.host)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun interceptPageRequest(request: Request, okHttp: OkHttpClient): Response {
|
||||||
|
if (!settings.isImagesProxyEnabled) {
|
||||||
|
return okHttp.newCall(request).await()
|
||||||
|
}
|
||||||
|
val sourceUrl = request.url
|
||||||
|
val targetUrl = HttpUrl.Builder()
|
||||||
|
.scheme("https")
|
||||||
|
.host("wsrv.nl")
|
||||||
|
.addQueryParameter("url", sourceUrl.toString())
|
||||||
|
.addQueryParameter("we", null)
|
||||||
|
val newRequest = request.newBuilder()
|
||||||
|
.url(targetUrl.build())
|
||||||
|
.build()
|
||||||
|
return runCatchingCancellable {
|
||||||
|
okHttp.doCall(newRequest)
|
||||||
|
}.recover {
|
||||||
|
logDebug(it)
|
||||||
|
okHttp.doCall(request).also {
|
||||||
|
blacklist.add(sourceUrl.host)
|
||||||
|
}
|
||||||
|
}.getOrThrow()
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun OkHttpClient.doCall(request: Request): Response {
|
||||||
|
return newCall(request).await().ensureSuccess()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun logDebug(e: Throwable?) {
|
||||||
|
if (BuildConfig.DEBUG) {
|
||||||
|
Log.w("ImageProxy", e.toString())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,45 @@
|
|||||||
|
package org.koitharu.kotatsu.core.network
|
||||||
|
|
||||||
|
import okhttp3.Authenticator
|
||||||
|
import okhttp3.Credentials
|
||||||
|
import okhttp3.Request
|
||||||
|
import okhttp3.Response
|
||||||
|
import okhttp3.Route
|
||||||
|
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||||
|
import java.net.PasswordAuthentication
|
||||||
|
import java.net.Proxy
|
||||||
|
|
||||||
|
class ProxyAuthenticator(
|
||||||
|
private val settings: AppSettings,
|
||||||
|
) : Authenticator, java.net.Authenticator() {
|
||||||
|
|
||||||
|
init {
|
||||||
|
setDefault(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun authenticate(route: Route?, response: Response): Request? {
|
||||||
|
if (!isProxyEnabled()) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
if (response.request.header(CommonHeaders.PROXY_AUTHORIZATION) != null) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
val login = settings.proxyLogin ?: return null
|
||||||
|
val password = settings.proxyPassword ?: return null
|
||||||
|
val credential = Credentials.basic(login, password)
|
||||||
|
return response.request.newBuilder()
|
||||||
|
.header(CommonHeaders.PROXY_AUTHORIZATION, credential)
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getPasswordAuthentication(): PasswordAuthentication? {
|
||||||
|
if (!isProxyEnabled()) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
val login = settings.proxyLogin ?: return null
|
||||||
|
val password = settings.proxyPassword ?: return null
|
||||||
|
return PasswordAuthentication(login, password.toCharArray())
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun isProxyEnabled() = settings.proxyType != Proxy.Type.DIRECT
|
||||||
|
}
|
||||||
@ -1,30 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.core.ui.list
|
|
||||||
|
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
|
||||||
|
|
||||||
class ScrollListenerInvalidationObserver(
|
|
||||||
private val recyclerView: RecyclerView,
|
|
||||||
private val scrollListener: RecyclerView.OnScrollListener,
|
|
||||||
) : RecyclerView.AdapterDataObserver() {
|
|
||||||
|
|
||||||
override fun onChanged() {
|
|
||||||
super.onChanged()
|
|
||||||
invalidateScroll()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onItemRangeInserted(positionStart: Int, itemCount: Int) {
|
|
||||||
super.onItemRangeInserted(positionStart, itemCount)
|
|
||||||
invalidateScroll()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onItemRangeRemoved(positionStart: Int, itemCount: Int) {
|
|
||||||
super.onItemRangeRemoved(positionStart, itemCount)
|
|
||||||
invalidateScroll()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun invalidateScroll() {
|
|
||||||
recyclerView.post {
|
|
||||||
scrollListener.onScrolled(recyclerView, 0, 0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -0,0 +1,123 @@
|
|||||||
|
package org.koitharu.kotatsu.core.ui.sheet
|
||||||
|
|
||||||
|
import android.app.Dialog
|
||||||
|
import android.view.View
|
||||||
|
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||||
|
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
||||||
|
import com.google.android.material.bottomsheet.BottomSheetBehavior.BottomSheetCallback
|
||||||
|
import com.google.android.material.bottomsheet.BottomSheetDialog
|
||||||
|
import com.google.android.material.sidesheet.SideSheetBehavior
|
||||||
|
import com.google.android.material.sidesheet.SideSheetCallback
|
||||||
|
import com.google.android.material.sidesheet.SideSheetDialog
|
||||||
|
import java.util.LinkedList
|
||||||
|
|
||||||
|
sealed class AdaptiveSheetBehavior {
|
||||||
|
|
||||||
|
@JvmField
|
||||||
|
protected val callbacks = LinkedList<AdaptiveSheetCallback>()
|
||||||
|
|
||||||
|
abstract var state: Int
|
||||||
|
|
||||||
|
abstract var isDraggable: Boolean
|
||||||
|
|
||||||
|
open val isHideable: Boolean = true
|
||||||
|
|
||||||
|
fun addCallback(callback: AdaptiveSheetCallback) {
|
||||||
|
callbacks.add(callback)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun removeCallback(callback: AdaptiveSheetCallback) {
|
||||||
|
callbacks.remove(callback)
|
||||||
|
}
|
||||||
|
|
||||||
|
class Bottom(
|
||||||
|
private val delegate: BottomSheetBehavior<*>,
|
||||||
|
) : AdaptiveSheetBehavior() {
|
||||||
|
|
||||||
|
override var state: Int
|
||||||
|
get() = delegate.state
|
||||||
|
set(value) {
|
||||||
|
delegate.state = value
|
||||||
|
}
|
||||||
|
|
||||||
|
override var isDraggable: Boolean
|
||||||
|
get() = delegate.isDraggable
|
||||||
|
set(value) {
|
||||||
|
delegate.isDraggable = value
|
||||||
|
}
|
||||||
|
|
||||||
|
override val isHideable: Boolean
|
||||||
|
get() = delegate.isHideable
|
||||||
|
|
||||||
|
var isFitToContents: Boolean
|
||||||
|
get() = delegate.isFitToContents
|
||||||
|
set(value) {
|
||||||
|
delegate.isFitToContents = value
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
delegate.addBottomSheetCallback(
|
||||||
|
object : BottomSheetCallback() {
|
||||||
|
override fun onStateChanged(bottomSheet: View, newState: Int) {
|
||||||
|
callbacks.forEach { it.onStateChanged(bottomSheet, newState) }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSlide(bottomSheet: View, slideOffset: Float) {
|
||||||
|
callbacks.forEach { it.onSlide(bottomSheet, slideOffset) }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Side(
|
||||||
|
private val delegate: SideSheetBehavior<*>,
|
||||||
|
) : AdaptiveSheetBehavior() {
|
||||||
|
|
||||||
|
override var state: Int
|
||||||
|
get() = delegate.state
|
||||||
|
set(value) {
|
||||||
|
delegate.state = value
|
||||||
|
}
|
||||||
|
|
||||||
|
override var isDraggable: Boolean
|
||||||
|
get() = delegate.isDraggable
|
||||||
|
set(value) {
|
||||||
|
delegate.isDraggable = value
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
delegate.addCallback(
|
||||||
|
object : SideSheetCallback() {
|
||||||
|
override fun onStateChanged(sheet: View, newState: Int) {
|
||||||
|
callbacks.forEach { it.onStateChanged(sheet, newState) }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSlide(sheet: View, slideOffset: Float) {
|
||||||
|
callbacks.forEach { it.onSlide(sheet, slideOffset) }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
const val STATE_EXPANDED = SideSheetBehavior.STATE_EXPANDED
|
||||||
|
const val STATE_SETTLING = SideSheetBehavior.STATE_SETTLING
|
||||||
|
const val STATE_DRAGGING = SideSheetBehavior.STATE_DRAGGING
|
||||||
|
const val STATE_HIDDEN = SideSheetBehavior.STATE_HIDDEN
|
||||||
|
|
||||||
|
fun from(dialog: Dialog?): AdaptiveSheetBehavior? = when (dialog) {
|
||||||
|
is BottomSheetDialog -> Bottom(dialog.behavior)
|
||||||
|
is SideSheetDialog -> Side(dialog.behavior)
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
|
||||||
|
fun from(lp: CoordinatorLayout.LayoutParams): AdaptiveSheetBehavior? = when (val behavior = lp.behavior) {
|
||||||
|
is BottomSheetBehavior<*> -> Bottom(behavior)
|
||||||
|
is SideSheetBehavior<*> -> Side(behavior)
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,22 @@
|
|||||||
|
package org.koitharu.kotatsu.core.ui.sheet
|
||||||
|
|
||||||
|
import android.view.View
|
||||||
|
|
||||||
|
interface AdaptiveSheetCallback {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when the sheet changes its state.
|
||||||
|
*
|
||||||
|
* @param sheet The sheet view.
|
||||||
|
* @param newState The new state.
|
||||||
|
*/
|
||||||
|
fun onStateChanged(sheet: View, newState: Int)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when the sheet is being dragged.
|
||||||
|
*
|
||||||
|
* @param sheet The sheet view.
|
||||||
|
* @param slideOffset The new offset of this sheet.
|
||||||
|
*/
|
||||||
|
fun onSlide(sheet: View, slideOffset: Float) = Unit
|
||||||
|
}
|
||||||
@ -0,0 +1,94 @@
|
|||||||
|
package org.koitharu.kotatsu.core.ui.sheet
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.util.AttributeSet
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.widget.LinearLayout
|
||||||
|
import androidx.annotation.AttrRes
|
||||||
|
import androidx.annotation.StringRes
|
||||||
|
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||||
|
import androidx.core.content.withStyledAttributes
|
||||||
|
import androidx.core.view.isGone
|
||||||
|
import androidx.core.view.isVisible
|
||||||
|
import org.koitharu.kotatsu.R
|
||||||
|
import org.koitharu.kotatsu.core.util.ext.parents
|
||||||
|
import org.koitharu.kotatsu.databinding.LayoutSheetHeaderAdaptiveBinding
|
||||||
|
|
||||||
|
class AdaptiveSheetHeaderBar @JvmOverloads constructor(
|
||||||
|
context: Context,
|
||||||
|
attrs: AttributeSet? = null,
|
||||||
|
@AttrRes defStyleAttr: Int = 0,
|
||||||
|
) : LinearLayout(context, attrs, defStyleAttr), AdaptiveSheetCallback {
|
||||||
|
|
||||||
|
private val binding = LayoutSheetHeaderAdaptiveBinding.inflate(LayoutInflater.from(context), this)
|
||||||
|
private var sheetBehavior: AdaptiveSheetBehavior? = null
|
||||||
|
|
||||||
|
var title: CharSequence?
|
||||||
|
get() = binding.shTextViewTitle.text
|
||||||
|
set(value) {
|
||||||
|
binding.shTextViewTitle.text = value
|
||||||
|
}
|
||||||
|
|
||||||
|
val isTitleVisible: Boolean
|
||||||
|
get() = binding.shLayoutSidesheet.isVisible
|
||||||
|
|
||||||
|
init {
|
||||||
|
orientation = VERTICAL
|
||||||
|
binding.shButtonClose.setOnClickListener { dismissSheet() }
|
||||||
|
context.withStyledAttributes(
|
||||||
|
attrs,
|
||||||
|
R.styleable.AdaptiveSheetHeaderBar, defStyleAttr,
|
||||||
|
) {
|
||||||
|
title = getText(R.styleable.AdaptiveSheetHeaderBar_title)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onAttachedToWindow() {
|
||||||
|
super.onAttachedToWindow()
|
||||||
|
if (isInEditMode) {
|
||||||
|
val isTabled = resources.getBoolean(R.bool.is_tablet)
|
||||||
|
binding.shDragHandle.isGone = isTabled
|
||||||
|
binding.shLayoutSidesheet.isVisible = isTabled
|
||||||
|
} else {
|
||||||
|
setBottomSheetBehavior(findParentSheetBehavior())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDetachedFromWindow() {
|
||||||
|
setBottomSheetBehavior(null)
|
||||||
|
super.onDetachedFromWindow()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStateChanged(sheet: View, newState: Int) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setTitle(@StringRes resId: Int) {
|
||||||
|
binding.shTextViewTitle.setText(resId)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setBottomSheetBehavior(behavior: AdaptiveSheetBehavior?) {
|
||||||
|
binding.shDragHandle.isVisible = behavior is AdaptiveSheetBehavior.Bottom
|
||||||
|
binding.shLayoutSidesheet.isVisible = behavior is AdaptiveSheetBehavior.Side
|
||||||
|
sheetBehavior?.removeCallback(this)
|
||||||
|
sheetBehavior = behavior
|
||||||
|
behavior?.addCallback(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun dismissSheet() {
|
||||||
|
sheetBehavior?.state = AdaptiveSheetBehavior.STATE_HIDDEN
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun findParentSheetBehavior(): AdaptiveSheetBehavior? {
|
||||||
|
for (p in parents) {
|
||||||
|
val layoutParams = (p as? View)?.layoutParams
|
||||||
|
if (layoutParams is CoordinatorLayout.LayoutParams) {
|
||||||
|
AdaptiveSheetBehavior.from(layoutParams)?.let {
|
||||||
|
return it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,175 @@
|
|||||||
|
package org.koitharu.kotatsu.core.ui.sheet
|
||||||
|
|
||||||
|
import android.app.Dialog
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.view.ViewGroup.LayoutParams
|
||||||
|
import androidx.activity.ComponentDialog
|
||||||
|
import androidx.activity.OnBackPressedDispatcher
|
||||||
|
import androidx.appcompat.app.AppCompatDialogFragment
|
||||||
|
import androidx.core.view.updateLayoutParams
|
||||||
|
import androidx.viewbinding.ViewBinding
|
||||||
|
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
||||||
|
import com.google.android.material.bottomsheet.BottomSheetDialog
|
||||||
|
import com.google.android.material.sidesheet.SideSheetDialog
|
||||||
|
import org.koitharu.kotatsu.R
|
||||||
|
import com.google.android.material.R as materialR
|
||||||
|
|
||||||
|
abstract class BaseAdaptiveSheet<B : ViewBinding> : AppCompatDialogFragment() {
|
||||||
|
|
||||||
|
private var waitingForDismissAllowingStateLoss = false
|
||||||
|
private var isFitToContentsDisabled = false
|
||||||
|
|
||||||
|
var viewBinding: B? = null
|
||||||
|
private set
|
||||||
|
|
||||||
|
@Deprecated("", ReplaceWith("requireViewBinding()"))
|
||||||
|
protected val binding: B
|
||||||
|
get() = requireViewBinding()
|
||||||
|
|
||||||
|
protected val behavior: AdaptiveSheetBehavior?
|
||||||
|
get() = AdaptiveSheetBehavior.from(dialog)
|
||||||
|
|
||||||
|
val isExpanded: Boolean
|
||||||
|
get() = behavior?.state == AdaptiveSheetBehavior.STATE_EXPANDED
|
||||||
|
|
||||||
|
val onBackPressedDispatcher: OnBackPressedDispatcher
|
||||||
|
get() = (requireDialog() as ComponentDialog).onBackPressedDispatcher
|
||||||
|
|
||||||
|
final override fun onCreateView(
|
||||||
|
inflater: LayoutInflater,
|
||||||
|
container: ViewGroup?,
|
||||||
|
savedInstanceState: Bundle?,
|
||||||
|
): View {
|
||||||
|
val binding = onCreateViewBinding(inflater, container)
|
||||||
|
viewBinding = binding
|
||||||
|
return binding.root
|
||||||
|
}
|
||||||
|
|
||||||
|
final override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
val binding = requireViewBinding()
|
||||||
|
onViewBindingCreated(binding, savedInstanceState)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroyView() {
|
||||||
|
viewBinding = null
|
||||||
|
super.onDestroyView()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||||
|
val context = requireContext()
|
||||||
|
return if (context.resources.getBoolean(R.bool.is_tablet)) {
|
||||||
|
SideSheetDialog(context, theme)
|
||||||
|
} else {
|
||||||
|
BottomSheetDialog(context, theme)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun addSheetCallback(callback: AdaptiveSheetCallback) {
|
||||||
|
val b = behavior ?: return
|
||||||
|
b.addCallback(callback)
|
||||||
|
val rootView = dialog?.findViewById<View>(materialR.id.design_bottom_sheet)
|
||||||
|
?: dialog?.findViewById(materialR.id.coordinator)
|
||||||
|
if (rootView != null) {
|
||||||
|
callback.onStateChanged(rootView, b.state)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected abstract fun onCreateViewBinding(inflater: LayoutInflater, container: ViewGroup?): B
|
||||||
|
|
||||||
|
protected open fun onViewBindingCreated(binding: B, savedInstanceState: Bundle?) = Unit
|
||||||
|
|
||||||
|
protected fun setExpanded(isExpanded: Boolean, isLocked: Boolean) {
|
||||||
|
val b = behavior ?: return
|
||||||
|
if (isExpanded) {
|
||||||
|
b.state = BottomSheetBehavior.STATE_EXPANDED
|
||||||
|
}
|
||||||
|
if (b is AdaptiveSheetBehavior.Bottom) {
|
||||||
|
b.isFitToContents = !isFitToContentsDisabled && !isExpanded
|
||||||
|
val rootView = dialog?.findViewById<View>(materialR.id.design_bottom_sheet)
|
||||||
|
rootView?.updateLayoutParams {
|
||||||
|
height = if (isFitToContentsDisabled || isExpanded) {
|
||||||
|
LayoutParams.MATCH_PARENT
|
||||||
|
} else {
|
||||||
|
LayoutParams.WRAP_CONTENT
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
b.isDraggable = !isLocked
|
||||||
|
}
|
||||||
|
|
||||||
|
protected fun disableFitToContents() {
|
||||||
|
isFitToContentsDisabled = true
|
||||||
|
val b = behavior as? AdaptiveSheetBehavior.Bottom ?: return
|
||||||
|
b.isFitToContents = false
|
||||||
|
dialog?.findViewById<View>(materialR.id.design_bottom_sheet)?.updateLayoutParams {
|
||||||
|
height = LayoutParams.MATCH_PARENT
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun requireViewBinding(): B = checkNotNull(viewBinding) {
|
||||||
|
"Fragment $this did not return a ViewBinding from onCreateView() or this was called before onCreateView()."
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun dismiss() {
|
||||||
|
if (!tryDismissWithAnimation(false)) {
|
||||||
|
super.dismiss()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun dismissAllowingStateLoss() {
|
||||||
|
if (!tryDismissWithAnimation(true)) {
|
||||||
|
super.dismissAllowingStateLoss()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tries to dismiss the dialog fragment with the bottom sheet animation. Returns true if possible,
|
||||||
|
* false otherwise.
|
||||||
|
*/
|
||||||
|
private fun tryDismissWithAnimation(allowingStateLoss: Boolean): Boolean {
|
||||||
|
val shouldDismissWithAnimation = when (val dialog = dialog) {
|
||||||
|
is BottomSheetDialog -> dialog.dismissWithAnimation
|
||||||
|
is SideSheetDialog -> dialog.isDismissWithSheetAnimationEnabled
|
||||||
|
else -> false
|
||||||
|
}
|
||||||
|
val behavior = behavior ?: return false
|
||||||
|
return if (shouldDismissWithAnimation && behavior.isHideable) {
|
||||||
|
dismissWithAnimation(behavior, allowingStateLoss)
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun dismissWithAnimation(behavior: AdaptiveSheetBehavior, allowingStateLoss: Boolean) {
|
||||||
|
waitingForDismissAllowingStateLoss = allowingStateLoss
|
||||||
|
if (behavior.state == AdaptiveSheetBehavior.STATE_HIDDEN) {
|
||||||
|
dismissAfterAnimation()
|
||||||
|
} else {
|
||||||
|
behavior.addCallback(SheetDismissCallback())
|
||||||
|
behavior.state = AdaptiveSheetBehavior.STATE_HIDDEN
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun dismissAfterAnimation() {
|
||||||
|
if (waitingForDismissAllowingStateLoss) {
|
||||||
|
super.dismissAllowingStateLoss()
|
||||||
|
} else {
|
||||||
|
super.dismiss()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private inner class SheetDismissCallback : AdaptiveSheetCallback {
|
||||||
|
override fun onStateChanged(sheet: View, newState: Int) {
|
||||||
|
if (newState == BottomSheetBehavior.STATE_HIDDEN) {
|
||||||
|
dismissAfterAnimation()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSlide(sheet: View, slideOffset: Float) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,12 @@
|
|||||||
|
package org.koitharu.kotatsu.core.util
|
||||||
|
|
||||||
|
class CompositeRunnable(
|
||||||
|
private val children: List<Runnable>,
|
||||||
|
) : Runnable, Collection<Runnable> by children {
|
||||||
|
|
||||||
|
override fun run() {
|
||||||
|
for (child in children) {
|
||||||
|
child.run()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,23 @@
|
|||||||
|
package org.koitharu.kotatsu.core.util.ext
|
||||||
|
|
||||||
|
import android.view.View
|
||||||
|
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
||||||
|
import com.google.android.material.bottomsheet.BottomSheetBehavior.BottomSheetCallback
|
||||||
|
|
||||||
|
fun BottomSheetBehavior<*>.doOnExpansionsChanged(callback: (isExpanded: Boolean) -> Unit) {
|
||||||
|
var isExpended = state == BottomSheetBehavior.STATE_EXPANDED
|
||||||
|
callback(isExpended)
|
||||||
|
addBottomSheetCallback(
|
||||||
|
object : BottomSheetCallback() {
|
||||||
|
override fun onStateChanged(bottomSheet: View, newState: Int) {
|
||||||
|
val expanded = newState == BottomSheetBehavior.STATE_EXPANDED
|
||||||
|
if (expanded != isExpended) {
|
||||||
|
isExpended = expanded
|
||||||
|
callback(expanded)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSlide(bottomSheet: View, slideOffset: Float) = Unit
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -0,0 +1,45 @@
|
|||||||
|
package org.koitharu.kotatsu.details.ui
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.core.view.doOnLayout
|
||||||
|
import androidx.core.widget.NestedScrollView
|
||||||
|
import org.koitharu.kotatsu.core.util.ext.findActivity
|
||||||
|
import java.lang.ref.WeakReference
|
||||||
|
|
||||||
|
class TitleScrollCoordinator(
|
||||||
|
private val titleView: TextView,
|
||||||
|
) : NestedScrollView.OnScrollChangeListener {
|
||||||
|
|
||||||
|
private val location = IntArray(2)
|
||||||
|
private var activityRef: WeakReference<AppCompatActivity>? = null
|
||||||
|
|
||||||
|
override fun onScrollChange(v: NestedScrollView, scrollX: Int, scrollY: Int, oldScrollX: Int, oldScrollY: Int) {
|
||||||
|
val actionBar = getActivity(v.context)?.supportActionBar ?: return
|
||||||
|
titleView.getLocationOnScreen(location)
|
||||||
|
var top = location[1] + titleView.height
|
||||||
|
v.getLocationOnScreen(location)
|
||||||
|
top -= location[1]
|
||||||
|
actionBar.setDisplayShowTitleEnabled(top < 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun attach(scrollView: NestedScrollView) {
|
||||||
|
scrollView.setOnScrollChangeListener(this)
|
||||||
|
scrollView.doOnLayout {
|
||||||
|
onScrollChange(scrollView, 0, 0, 0, 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getActivity(context: Context): AppCompatActivity? {
|
||||||
|
activityRef?.get()?.let {
|
||||||
|
if (!it.isDestroyed) return it
|
||||||
|
}
|
||||||
|
val activity = context.findActivity() as? AppCompatActivity
|
||||||
|
if (activity == null || activity.isDestroyed) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
activityRef = WeakReference(activity)
|
||||||
|
return activity
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,32 @@
|
|||||||
|
package org.koitharu.kotatsu.favourites.domain.model
|
||||||
|
|
||||||
|
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||||
|
|
||||||
|
class Cover(
|
||||||
|
val url: String,
|
||||||
|
val source: String,
|
||||||
|
) {
|
||||||
|
|
||||||
|
val mangaSource: MangaSource?
|
||||||
|
get() = if (source.isEmpty()) null else MangaSource.values().find { it.name == source }
|
||||||
|
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
if (this === other) return true
|
||||||
|
if (javaClass != other?.javaClass) return false
|
||||||
|
|
||||||
|
other as Cover
|
||||||
|
|
||||||
|
if (url != other.url) return false
|
||||||
|
return source == other.source
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hashCode(): Int {
|
||||||
|
var result = url.hashCode()
|
||||||
|
result = 31 * result + source.hashCode()
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun toString(): String {
|
||||||
|
return "Cover(url='$url', source=$source)"
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,27 @@
|
|||||||
|
package org.koitharu.kotatsu.favourites.ui.categories.select.adapter
|
||||||
|
|
||||||
|
import android.view.View
|
||||||
|
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
|
||||||
|
import org.koitharu.kotatsu.R
|
||||||
|
import org.koitharu.kotatsu.databinding.ItemCategoriesHeaderBinding
|
||||||
|
import org.koitharu.kotatsu.favourites.ui.categories.FavouriteCategoriesActivity
|
||||||
|
import org.koitharu.kotatsu.favourites.ui.categories.edit.FavouritesCategoryEditActivity
|
||||||
|
import org.koitharu.kotatsu.favourites.ui.categories.select.model.CategoriesHeaderItem
|
||||||
|
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||||
|
|
||||||
|
fun categoriesHeaderAD() = adapterDelegateViewBinding<CategoriesHeaderItem, ListModel, ItemCategoriesHeaderBinding>(
|
||||||
|
{ inflater, parent -> ItemCategoriesHeaderBinding.inflate(inflater, parent, false) },
|
||||||
|
) {
|
||||||
|
|
||||||
|
val onClickListener = View.OnClickListener { v ->
|
||||||
|
val intent = when (v.id) {
|
||||||
|
R.id.button_create -> FavouritesCategoryEditActivity.newIntent(v.context)
|
||||||
|
R.id.button_manage -> FavouriteCategoriesActivity.newIntent(v.context)
|
||||||
|
else -> return@OnClickListener
|
||||||
|
}
|
||||||
|
v.context.startActivity(intent)
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.buttonCreate.setOnClickListener(onClickListener)
|
||||||
|
binding.buttonManage.setOnClickListener(onClickListener)
|
||||||
|
}
|
||||||
@ -0,0 +1,8 @@
|
|||||||
|
package org.koitharu.kotatsu.favourites.ui.categories.select.model
|
||||||
|
|
||||||
|
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||||
|
|
||||||
|
class CategoriesHeaderItem : ListModel {
|
||||||
|
|
||||||
|
override fun equals(other: Any?): Boolean = other?.javaClass == CategoriesHeaderItem::class.java
|
||||||
|
}
|
||||||
@ -1,7 +1,9 @@
|
|||||||
package org.koitharu.kotatsu.favourites.ui.categories.select.model
|
package org.koitharu.kotatsu.favourites.ui.categories.select.model
|
||||||
|
|
||||||
|
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||||
|
|
||||||
data class MangaCategoryItem(
|
data class MangaCategoryItem(
|
||||||
val id: Long,
|
val id: Long,
|
||||||
val name: String,
|
val name: String,
|
||||||
val isChecked: Boolean
|
val isChecked: Boolean,
|
||||||
)
|
) : ListModel
|
||||||
|
|||||||
@ -0,0 +1,46 @@
|
|||||||
|
package org.koitharu.kotatsu.filter.ui
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.recyclerview.widget.AsyncListDiffer.ListListener
|
||||||
|
import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter
|
||||||
|
import org.koitharu.kotatsu.core.ui.list.fastscroll.FastScroller
|
||||||
|
import org.koitharu.kotatsu.filter.ui.model.FilterItem
|
||||||
|
import org.koitharu.kotatsu.list.ui.adapter.listSimpleHeaderAD
|
||||||
|
import org.koitharu.kotatsu.list.ui.adapter.loadingFooterAD
|
||||||
|
import org.koitharu.kotatsu.list.ui.adapter.loadingStateAD
|
||||||
|
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||||
|
|
||||||
|
class FilterAdapter(
|
||||||
|
listener: OnFilterChangedListener,
|
||||||
|
listListener: ListListener<ListModel>,
|
||||||
|
) : AsyncListDifferDelegationAdapter<ListModel>(FilterDiffCallback()), FastScroller.SectionIndexer {
|
||||||
|
|
||||||
|
init {
|
||||||
|
delegatesManager
|
||||||
|
.addDelegate(filterSortDelegate(listener))
|
||||||
|
.addDelegate(filterTagDelegate(listener))
|
||||||
|
.addDelegate(listSimpleHeaderAD())
|
||||||
|
.addDelegate(loadingStateAD())
|
||||||
|
.addDelegate(loadingFooterAD())
|
||||||
|
.addDelegate(filterErrorDelegate())
|
||||||
|
differ.addListListener(listListener)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getSectionText(context: Context, position: Int): CharSequence? {
|
||||||
|
val list = items
|
||||||
|
for (i in (0..position).reversed()) {
|
||||||
|
val item = list.getOrNull(i) ?: continue
|
||||||
|
if (item is FilterItem.Tag) {
|
||||||
|
return item.tag.title.firstOrNull()?.toString()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
const val ITEM_TYPE_HEADER = 0
|
||||||
|
const val ITEM_TYPE_SORT = 1
|
||||||
|
const val ITEM_TYPE_TAG = 2
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,51 @@
|
|||||||
|
package org.koitharu.kotatsu.filter.ui
|
||||||
|
|
||||||
|
import android.widget.TextView
|
||||||
|
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegate
|
||||||
|
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
|
||||||
|
import org.koitharu.kotatsu.R
|
||||||
|
import org.koitharu.kotatsu.core.ui.model.titleRes
|
||||||
|
import org.koitharu.kotatsu.core.util.ext.setChecked
|
||||||
|
import org.koitharu.kotatsu.databinding.ItemCheckableMultipleBinding
|
||||||
|
import org.koitharu.kotatsu.databinding.ItemCheckableSingleBinding
|
||||||
|
import org.koitharu.kotatsu.filter.ui.model.FilterItem
|
||||||
|
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||||
|
|
||||||
|
fun filterSortDelegate(
|
||||||
|
listener: OnFilterChangedListener,
|
||||||
|
) = adapterDelegateViewBinding<FilterItem.Sort, ListModel, ItemCheckableSingleBinding>(
|
||||||
|
{ layoutInflater, parent -> ItemCheckableSingleBinding.inflate(layoutInflater, parent, false) },
|
||||||
|
) {
|
||||||
|
|
||||||
|
itemView.setOnClickListener {
|
||||||
|
listener.onSortItemClick(item)
|
||||||
|
}
|
||||||
|
|
||||||
|
bind { payloads ->
|
||||||
|
binding.root.setText(item.order.titleRes)
|
||||||
|
binding.root.setChecked(item.isSelected, payloads.isNotEmpty())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun filterTagDelegate(
|
||||||
|
listener: OnFilterChangedListener,
|
||||||
|
) = adapterDelegateViewBinding<FilterItem.Tag, ListModel, ItemCheckableMultipleBinding>(
|
||||||
|
{ layoutInflater, parent -> ItemCheckableMultipleBinding.inflate(layoutInflater, parent, false) },
|
||||||
|
) {
|
||||||
|
|
||||||
|
itemView.setOnClickListener {
|
||||||
|
listener.onTagItemClick(item)
|
||||||
|
}
|
||||||
|
|
||||||
|
bind { payloads ->
|
||||||
|
binding.root.text = item.tag.title
|
||||||
|
binding.root.setChecked(item.isChecked, payloads.isNotEmpty())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun filterErrorDelegate() = adapterDelegate<FilterItem.Error, ListModel>(R.layout.item_sources_empty) {
|
||||||
|
|
||||||
|
bind {
|
||||||
|
(itemView as TextView).setText(item.textResId)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,52 @@
|
|||||||
|
package org.koitharu.kotatsu.filter.ui
|
||||||
|
|
||||||
|
import androidx.recyclerview.widget.DiffUtil
|
||||||
|
import org.koitharu.kotatsu.filter.ui.model.FilterItem
|
||||||
|
import org.koitharu.kotatsu.list.ui.model.ListHeader
|
||||||
|
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||||
|
|
||||||
|
class FilterDiffCallback : DiffUtil.ItemCallback<ListModel>() {
|
||||||
|
|
||||||
|
override fun areItemsTheSame(oldItem: ListModel, newItem: ListModel): Boolean {
|
||||||
|
return when {
|
||||||
|
oldItem === newItem -> true
|
||||||
|
oldItem.javaClass != newItem.javaClass -> false
|
||||||
|
oldItem is ListHeader && newItem is ListHeader -> {
|
||||||
|
oldItem == newItem
|
||||||
|
}
|
||||||
|
|
||||||
|
oldItem is FilterItem.Tag && newItem is FilterItem.Tag -> {
|
||||||
|
oldItem.tag == newItem.tag
|
||||||
|
}
|
||||||
|
|
||||||
|
oldItem is FilterItem.Sort && newItem is FilterItem.Sort -> {
|
||||||
|
oldItem.order == newItem.order
|
||||||
|
}
|
||||||
|
|
||||||
|
oldItem is FilterItem.Error && newItem is FilterItem.Error -> {
|
||||||
|
oldItem.textResId == newItem.textResId
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun areContentsTheSame(oldItem: ListModel, newItem: ListModel): Boolean {
|
||||||
|
return oldItem == newItem
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getChangePayload(oldItem: ListModel, newItem: ListModel): Any? {
|
||||||
|
val hasPayload = when {
|
||||||
|
oldItem is FilterItem.Tag && newItem is FilterItem.Tag -> {
|
||||||
|
oldItem.isChecked != newItem.isChecked
|
||||||
|
}
|
||||||
|
|
||||||
|
oldItem is FilterItem.Sort && newItem is FilterItem.Sort -> {
|
||||||
|
oldItem.isSelected != newItem.isSelected
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> false
|
||||||
|
}
|
||||||
|
return if (hasPayload) Unit else super.getChangePayload(oldItem, newItem)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,71 @@
|
|||||||
|
package org.koitharu.kotatsu.filter.ui
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.core.graphics.Insets
|
||||||
|
import androidx.core.view.isVisible
|
||||||
|
import com.google.android.material.chip.Chip
|
||||||
|
import org.koitharu.kotatsu.R
|
||||||
|
import org.koitharu.kotatsu.core.ui.BaseFragment
|
||||||
|
import org.koitharu.kotatsu.core.ui.widgets.ChipsView
|
||||||
|
import org.koitharu.kotatsu.core.util.ext.isAnimationsEnabled
|
||||||
|
import org.koitharu.kotatsu.core.util.ext.observe
|
||||||
|
import org.koitharu.kotatsu.databinding.FragmentFilterHeaderBinding
|
||||||
|
import org.koitharu.kotatsu.filter.ui.model.FilterHeaderModel
|
||||||
|
import org.koitharu.kotatsu.filter.ui.model.FilterItem
|
||||||
|
import org.koitharu.kotatsu.parsers.model.MangaTag
|
||||||
|
import com.google.android.material.R as materialR
|
||||||
|
|
||||||
|
class FilterHeaderFragment : BaseFragment<FragmentFilterHeaderBinding>(), ChipsView.OnChipClickListener {
|
||||||
|
|
||||||
|
private val owner by lazy(LazyThreadSafetyMode.NONE) {
|
||||||
|
FilterOwner.from(requireActivity())
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateViewBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentFilterHeaderBinding {
|
||||||
|
return FragmentFilterHeaderBinding.inflate(inflater, container, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onViewBindingCreated(binding: FragmentFilterHeaderBinding, savedInstanceState: Bundle?) {
|
||||||
|
super.onViewBindingCreated(binding, savedInstanceState)
|
||||||
|
binding.chipsTags.onChipClickListener = this
|
||||||
|
owner.header.observe(viewLifecycleOwner, ::onDataChanged)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onWindowInsetsChanged(insets: Insets) = Unit
|
||||||
|
|
||||||
|
override fun onChipClick(chip: Chip, data: Any?) {
|
||||||
|
val tag = data as? MangaTag
|
||||||
|
if (tag == null) {
|
||||||
|
FilterSheetFragment.show(parentFragmentManager)
|
||||||
|
} else {
|
||||||
|
owner.onTagItemClick(FilterItem.Tag(tag, !chip.isChecked))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onDataChanged(header: FilterHeaderModel) {
|
||||||
|
val binding = viewBinding ?: return
|
||||||
|
val chips = header.chips
|
||||||
|
if (chips.isEmpty()) {
|
||||||
|
binding.chipsTags.setChips(emptyList())
|
||||||
|
binding.root.isVisible = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (binding.root.context.isAnimationsEnabled) {
|
||||||
|
binding.scrollView.smoothScrollTo(0, 0)
|
||||||
|
} else {
|
||||||
|
binding.scrollView.scrollTo(0, 0)
|
||||||
|
}
|
||||||
|
binding.chipsTags.setChips(header.chips + moreTagsChip())
|
||||||
|
binding.root.isVisible = true
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun moreTagsChip() = ChipsView.ChipModel(
|
||||||
|
tint = 0,
|
||||||
|
title = getString(R.string.more),
|
||||||
|
icon = materialR.drawable.abc_ic_menu_overflow_material,
|
||||||
|
isCheckable = false,
|
||||||
|
isChecked = false,
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -0,0 +1,32 @@
|
|||||||
|
package org.koitharu.kotatsu.filter.ui
|
||||||
|
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
import androidx.fragment.app.FragmentActivity
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import org.koitharu.kotatsu.core.util.ext.values
|
||||||
|
import org.koitharu.kotatsu.filter.ui.model.FilterHeaderModel
|
||||||
|
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||||
|
import org.koitharu.kotatsu.parsers.model.MangaTag
|
||||||
|
|
||||||
|
interface FilterOwner : OnFilterChangedListener {
|
||||||
|
|
||||||
|
val filterItems: StateFlow<List<ListModel>>
|
||||||
|
|
||||||
|
val header: StateFlow<FilterHeaderModel>
|
||||||
|
|
||||||
|
fun applyFilter(tags: Set<MangaTag>)
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
fun from(activity: FragmentActivity): FilterOwner {
|
||||||
|
for (f in activity.supportFragmentManager.fragments) {
|
||||||
|
return find(f) ?: continue
|
||||||
|
}
|
||||||
|
error("Cannot find FilterOwner")
|
||||||
|
}
|
||||||
|
|
||||||
|
fun find(fragment: Fragment): FilterOwner? {
|
||||||
|
return fragment.viewModelStore.values.firstNotNullOfOrNull { it as? FilterOwner }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,59 @@
|
|||||||
|
package org.koitharu.kotatsu.filter.ui
|
||||||
|
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.fragment.app.FragmentManager
|
||||||
|
import androidx.recyclerview.widget.AsyncListDiffer
|
||||||
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
|
import org.koitharu.kotatsu.core.ui.sheet.AdaptiveSheetBehavior
|
||||||
|
import org.koitharu.kotatsu.core.ui.sheet.AdaptiveSheetCallback
|
||||||
|
import org.koitharu.kotatsu.core.ui.sheet.BaseAdaptiveSheet
|
||||||
|
import org.koitharu.kotatsu.core.util.ext.observe
|
||||||
|
import org.koitharu.kotatsu.databinding.SheetFilterBinding
|
||||||
|
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||||
|
|
||||||
|
class FilterSheetFragment :
|
||||||
|
BaseAdaptiveSheet<SheetFilterBinding>(),
|
||||||
|
AdaptiveSheetCallback,
|
||||||
|
AsyncListDiffer.ListListener<ListModel> {
|
||||||
|
|
||||||
|
private val owner by lazy(LazyThreadSafetyMode.NONE) {
|
||||||
|
FilterOwner.from(requireActivity())
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateViewBinding(inflater: LayoutInflater, container: ViewGroup?): SheetFilterBinding {
|
||||||
|
return SheetFilterBinding.inflate(inflater, container, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onViewBindingCreated(binding: SheetFilterBinding, savedInstanceState: Bundle?) {
|
||||||
|
super.onViewBindingCreated(binding, savedInstanceState)
|
||||||
|
addSheetCallback(this)
|
||||||
|
val adapter = FilterAdapter(owner, this)
|
||||||
|
binding.recyclerView.adapter = adapter
|
||||||
|
owner.filterItems.observe(viewLifecycleOwner, adapter::setItems)
|
||||||
|
|
||||||
|
if (dialog == null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
|
binding.recyclerView.scrollIndicators = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCurrentListChanged(previousList: MutableList<ListModel>, currentList: MutableList<ListModel>) {
|
||||||
|
if (currentList.size > previousList.size && view != null) {
|
||||||
|
(requireViewBinding().recyclerView.layoutManager as LinearLayoutManager).scrollToPositionWithOffset(0, 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStateChanged(sheet: View, newState: Int) {
|
||||||
|
viewBinding?.recyclerView?.isFastScrollerEnabled = newState == AdaptiveSheetBehavior.STATE_EXPANDED
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
private const val TAG = "FilterBottomSheet"
|
||||||
|
|
||||||
|
fun show(fm: FragmentManager) = FilterSheetFragment().show(fm, TAG)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,8 +1,10 @@
|
|||||||
package org.koitharu.kotatsu.list.ui.filter
|
package org.koitharu.kotatsu.filter.ui
|
||||||
|
|
||||||
|
import org.koitharu.kotatsu.filter.ui.model.FilterItem
|
||||||
|
|
||||||
interface OnFilterChangedListener {
|
interface OnFilterChangedListener {
|
||||||
|
|
||||||
fun onSortItemClick(item: FilterItem.Sort)
|
fun onSortItemClick(item: FilterItem.Sort)
|
||||||
|
|
||||||
fun onTagItemClick(item: FilterItem.Tag)
|
fun onTagItemClick(item: FilterItem.Tag)
|
||||||
}
|
}
|
||||||
@ -1,19 +1,23 @@
|
|||||||
package org.koitharu.kotatsu.list.ui.model
|
package org.koitharu.kotatsu.filter.ui.model
|
||||||
|
|
||||||
import org.koitharu.kotatsu.core.ui.widgets.ChipsView
|
import org.koitharu.kotatsu.core.ui.widgets.ChipsView
|
||||||
|
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||||
import org.koitharu.kotatsu.parsers.model.SortOrder
|
import org.koitharu.kotatsu.parsers.model.SortOrder
|
||||||
|
|
||||||
class ListHeader2(
|
class FilterHeaderModel(
|
||||||
val chips: Collection<ChipsView.ChipModel>,
|
val chips: Collection<ChipsView.ChipModel>,
|
||||||
val sortOrder: SortOrder?,
|
val sortOrder: SortOrder?,
|
||||||
val hasSelectedTags: Boolean,
|
val hasSelectedTags: Boolean,
|
||||||
) : ListModel {
|
) : ListModel {
|
||||||
|
|
||||||
|
val textSummary: String
|
||||||
|
get() = chips.mapNotNull { if (it.isChecked) it.title else null }.joinToString()
|
||||||
|
|
||||||
override fun equals(other: Any?): Boolean {
|
override fun equals(other: Any?): Boolean {
|
||||||
if (this === other) return true
|
if (this === other) return true
|
||||||
if (javaClass != other?.javaClass) return false
|
if (javaClass != other?.javaClass) return false
|
||||||
|
|
||||||
other as ListHeader2
|
other as FilterHeaderModel
|
||||||
|
|
||||||
if (chips != other.chips) return false
|
if (chips != other.chips) return false
|
||||||
return sortOrder == other.sortOrder
|
return sortOrder == other.sortOrder
|
||||||
@ -0,0 +1,71 @@
|
|||||||
|
package org.koitharu.kotatsu.filter.ui.model
|
||||||
|
|
||||||
|
import androidx.annotation.StringRes
|
||||||
|
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||||
|
import org.koitharu.kotatsu.parsers.model.MangaTag
|
||||||
|
import org.koitharu.kotatsu.parsers.model.SortOrder
|
||||||
|
|
||||||
|
sealed interface FilterItem : ListModel {
|
||||||
|
|
||||||
|
class Sort(
|
||||||
|
val order: SortOrder,
|
||||||
|
val isSelected: Boolean,
|
||||||
|
) : FilterItem {
|
||||||
|
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
if (this === other) return true
|
||||||
|
if (javaClass != other?.javaClass) return false
|
||||||
|
|
||||||
|
other as Sort
|
||||||
|
|
||||||
|
if (order != other.order) return false
|
||||||
|
return isSelected == other.isSelected
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hashCode(): Int {
|
||||||
|
var result = order.hashCode()
|
||||||
|
result = 31 * result + isSelected.hashCode()
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Tag(
|
||||||
|
val tag: MangaTag,
|
||||||
|
val isChecked: Boolean,
|
||||||
|
) : FilterItem {
|
||||||
|
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
if (this === other) return true
|
||||||
|
if (javaClass != other?.javaClass) return false
|
||||||
|
|
||||||
|
other as Tag
|
||||||
|
|
||||||
|
if (tag != other.tag) return false
|
||||||
|
return isChecked == other.isChecked
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hashCode(): Int {
|
||||||
|
var result = tag.hashCode()
|
||||||
|
result = 31 * result + isChecked.hashCode()
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Error(
|
||||||
|
@StringRes val textResId: Int,
|
||||||
|
) : FilterItem {
|
||||||
|
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
if (this === other) return true
|
||||||
|
if (javaClass != other?.javaClass) return false
|
||||||
|
|
||||||
|
other as Error
|
||||||
|
|
||||||
|
return textResId == other.textResId
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hashCode(): Int {
|
||||||
|
return textResId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,32 @@
|
|||||||
|
package org.koitharu.kotatsu.list.domain
|
||||||
|
|
||||||
|
import dagger.Reusable
|
||||||
|
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||||
|
import org.koitharu.kotatsu.history.data.HistoryRepository
|
||||||
|
import org.koitharu.kotatsu.history.data.PROGRESS_NONE
|
||||||
|
import org.koitharu.kotatsu.tracker.domain.TrackingRepository
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@Reusable
|
||||||
|
class ListExtraProviderImpl @Inject constructor(
|
||||||
|
private val settings: AppSettings,
|
||||||
|
private val trackingRepository: TrackingRepository,
|
||||||
|
private val historyRepository: HistoryRepository,
|
||||||
|
) : ListExtraProvider {
|
||||||
|
|
||||||
|
override suspend fun getCounter(mangaId: Long): Int {
|
||||||
|
return if (settings.isTrackerEnabled) {
|
||||||
|
trackingRepository.getNewChaptersCount(mangaId)
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun getProgress(mangaId: Long): Float {
|
||||||
|
return if (settings.isReadingIndicatorsEnabled) {
|
||||||
|
historyRepository.getProgress(mangaId)
|
||||||
|
} else {
|
||||||
|
PROGRESS_NONE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,21 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.list.ui.filter
|
|
||||||
|
|
||||||
import androidx.recyclerview.widget.AsyncListDiffer.ListListener
|
|
||||||
import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter
|
|
||||||
|
|
||||||
class FilterAdapter(
|
|
||||||
listener: OnFilterChangedListener,
|
|
||||||
listListener: ListListener<FilterItem>,
|
|
||||||
) : AsyncListDifferDelegationAdapter<FilterItem>(
|
|
||||||
FilterDiffCallback(),
|
|
||||||
filterSortDelegate(listener),
|
|
||||||
filterTagDelegate(listener),
|
|
||||||
filterHeaderDelegate(),
|
|
||||||
filterLoadingDelegate(),
|
|
||||||
filterErrorDelegate(),
|
|
||||||
) {
|
|
||||||
|
|
||||||
init {
|
|
||||||
differ.addListListener(listListener)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,66 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.list.ui.filter
|
|
||||||
|
|
||||||
import android.widget.TextView
|
|
||||||
import androidx.core.view.isVisible
|
|
||||||
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegate
|
|
||||||
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
|
|
||||||
import org.koitharu.kotatsu.R
|
|
||||||
import org.koitharu.kotatsu.core.ui.model.titleRes
|
|
||||||
import org.koitharu.kotatsu.databinding.ItemCheckableNewBinding
|
|
||||||
import org.koitharu.kotatsu.databinding.ItemFilterHeaderBinding
|
|
||||||
|
|
||||||
fun filterSortDelegate(
|
|
||||||
listener: OnFilterChangedListener,
|
|
||||||
) = adapterDelegateViewBinding<FilterItem.Sort, FilterItem, ItemCheckableNewBinding>(
|
|
||||||
{ layoutInflater, parent -> ItemCheckableNewBinding.inflate(layoutInflater, parent, false) },
|
|
||||||
) {
|
|
||||||
|
|
||||||
itemView.setOnClickListener {
|
|
||||||
listener.onSortItemClick(item)
|
|
||||||
}
|
|
||||||
|
|
||||||
bind {
|
|
||||||
binding.root.setText(item.order.titleRes)
|
|
||||||
binding.root.isChecked = item.isSelected
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun filterTagDelegate(
|
|
||||||
listener: OnFilterChangedListener,
|
|
||||||
) = adapterDelegateViewBinding<FilterItem.Tag, FilterItem, ItemCheckableNewBinding>(
|
|
||||||
{ layoutInflater, parent -> ItemCheckableNewBinding.inflate(layoutInflater, parent, false) },
|
|
||||||
) {
|
|
||||||
|
|
||||||
itemView.setOnClickListener {
|
|
||||||
listener.onTagItemClick(item)
|
|
||||||
}
|
|
||||||
|
|
||||||
bind {
|
|
||||||
binding.root.text = item.tag.title
|
|
||||||
binding.root.isChecked = item.isChecked
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun filterHeaderDelegate() = adapterDelegateViewBinding<FilterItem.Header, FilterItem, ItemFilterHeaderBinding>(
|
|
||||||
{ layoutInflater, parent -> ItemFilterHeaderBinding.inflate(layoutInflater, parent, false) },
|
|
||||||
) {
|
|
||||||
|
|
||||||
bind {
|
|
||||||
binding.textViewTitle.setText(item.titleResId)
|
|
||||||
binding.badge.isVisible = if (item.counter == 0) {
|
|
||||||
false
|
|
||||||
} else {
|
|
||||||
binding.badge.text = item.counter.toString()
|
|
||||||
true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun filterLoadingDelegate() = adapterDelegate<FilterItem.Loading, FilterItem>(R.layout.item_loading_footer) {}
|
|
||||||
|
|
||||||
fun filterErrorDelegate() = adapterDelegate<FilterItem.Error, FilterItem>(R.layout.item_sources_empty) {
|
|
||||||
|
|
||||||
bind {
|
|
||||||
(itemView as TextView).setText(item.textResId)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,91 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.list.ui.filter
|
|
||||||
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.MenuItem
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import androidx.appcompat.widget.SearchView
|
|
||||||
import androidx.fragment.app.FragmentManager
|
|
||||||
import androidx.recyclerview.widget.AsyncListDiffer
|
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
|
||||||
import org.koitharu.kotatsu.R
|
|
||||||
import org.koitharu.kotatsu.core.ui.BaseBottomSheet
|
|
||||||
import org.koitharu.kotatsu.core.ui.util.CollapseActionViewCallback
|
|
||||||
import org.koitharu.kotatsu.core.util.ext.observe
|
|
||||||
import org.koitharu.kotatsu.core.util.ext.parentFragmentViewModels
|
|
||||||
import org.koitharu.kotatsu.databinding.SheetFilterBinding
|
|
||||||
import org.koitharu.kotatsu.remotelist.ui.RemoteListViewModel
|
|
||||||
|
|
||||||
class FilterBottomSheet :
|
|
||||||
BaseBottomSheet<SheetFilterBinding>(),
|
|
||||||
MenuItem.OnActionExpandListener,
|
|
||||||
SearchView.OnQueryTextListener,
|
|
||||||
AsyncListDiffer.ListListener<FilterItem> {
|
|
||||||
|
|
||||||
private val viewModel by parentFragmentViewModels<RemoteListViewModel>()
|
|
||||||
private var collapsibleActionViewCallback: CollapseActionViewCallback? = null
|
|
||||||
|
|
||||||
override fun onCreateViewBinding(inflater: LayoutInflater, container: ViewGroup?): SheetFilterBinding {
|
|
||||||
return SheetFilterBinding.inflate(inflater, container, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onViewBindingCreated(binding: SheetFilterBinding, savedInstanceState: Bundle?) {
|
|
||||||
super.onViewBindingCreated(binding, savedInstanceState)
|
|
||||||
val adapter = FilterAdapter(viewModel, this)
|
|
||||||
binding.recyclerView.adapter = adapter
|
|
||||||
viewModel.filterItems.observe(viewLifecycleOwner, adapter::setItems)
|
|
||||||
initOptionsMenu()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDestroyView() {
|
|
||||||
super.onDestroyView()
|
|
||||||
collapsibleActionViewCallback = null
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onMenuItemActionExpand(item: MenuItem): Boolean {
|
|
||||||
setExpanded(isExpanded = true, isLocked = true)
|
|
||||||
collapsibleActionViewCallback?.onMenuItemActionExpand(item)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onMenuItemActionCollapse(item: MenuItem): Boolean {
|
|
||||||
val searchView = (item.actionView as? SearchView) ?: return false
|
|
||||||
searchView.setQuery("", false)
|
|
||||||
searchView.post { setExpanded(isExpanded = false, isLocked = false) }
|
|
||||||
collapsibleActionViewCallback?.onMenuItemActionCollapse(item)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onQueryTextSubmit(query: String?): Boolean = false
|
|
||||||
|
|
||||||
override fun onQueryTextChange(newText: String?): Boolean {
|
|
||||||
viewModel.filterSearch(newText?.trim().orEmpty())
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCurrentListChanged(previousList: MutableList<FilterItem>, currentList: MutableList<FilterItem>) {
|
|
||||||
if (currentList.size > previousList.size && view != null) {
|
|
||||||
(requireViewBinding().recyclerView.layoutManager as LinearLayoutManager).scrollToPositionWithOffset(0, 0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun initOptionsMenu() {
|
|
||||||
requireViewBinding().headerBar.inflateMenu(R.menu.opt_filter)
|
|
||||||
val searchMenuItem = requireViewBinding().headerBar.menu.findItem(R.id.action_search)
|
|
||||||
searchMenuItem.setOnActionExpandListener(this)
|
|
||||||
val searchView = searchMenuItem.actionView as SearchView
|
|
||||||
searchView.setOnQueryTextListener(this)
|
|
||||||
searchView.setIconifiedByDefault(false)
|
|
||||||
searchView.queryHint = searchMenuItem.title
|
|
||||||
collapsibleActionViewCallback = CollapseActionViewCallback(searchMenuItem).also {
|
|
||||||
onBackPressedDispatcher.addCallback(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
|
|
||||||
private const val TAG = "FilterBottomSheet"
|
|
||||||
|
|
||||||
fun show(fm: FragmentManager) = FilterBottomSheet().show(fm, TAG)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,59 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.list.ui.filter
|
|
||||||
|
|
||||||
import androidx.recyclerview.widget.DiffUtil
|
|
||||||
|
|
||||||
class FilterDiffCallback : DiffUtil.ItemCallback<FilterItem>() {
|
|
||||||
|
|
||||||
override fun areItemsTheSame(oldItem: FilterItem, newItem: FilterItem): Boolean {
|
|
||||||
return when {
|
|
||||||
oldItem === newItem -> true
|
|
||||||
oldItem.javaClass != newItem.javaClass -> false
|
|
||||||
oldItem is FilterItem.Header && newItem is FilterItem.Header -> {
|
|
||||||
oldItem.titleResId == newItem.titleResId
|
|
||||||
}
|
|
||||||
oldItem is FilterItem.Tag && newItem is FilterItem.Tag -> {
|
|
||||||
oldItem.tag == newItem.tag
|
|
||||||
}
|
|
||||||
oldItem is FilterItem.Sort && newItem is FilterItem.Sort -> {
|
|
||||||
oldItem.order == newItem.order
|
|
||||||
}
|
|
||||||
oldItem is FilterItem.Error && newItem is FilterItem.Error -> {
|
|
||||||
oldItem.textResId == newItem.textResId
|
|
||||||
}
|
|
||||||
else -> false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun areContentsTheSame(oldItem: FilterItem, newItem: FilterItem): Boolean {
|
|
||||||
return when {
|
|
||||||
oldItem == FilterItem.Loading && newItem == FilterItem.Loading -> true
|
|
||||||
oldItem is FilterItem.Header && newItem is FilterItem.Header -> {
|
|
||||||
oldItem.counter == newItem.counter
|
|
||||||
}
|
|
||||||
oldItem is FilterItem.Error && newItem is FilterItem.Error -> true
|
|
||||||
oldItem is FilterItem.Tag && newItem is FilterItem.Tag -> {
|
|
||||||
oldItem.isChecked == newItem.isChecked
|
|
||||||
}
|
|
||||||
oldItem is FilterItem.Sort && newItem is FilterItem.Sort -> {
|
|
||||||
oldItem.isSelected == newItem.isSelected
|
|
||||||
}
|
|
||||||
else -> false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getChangePayload(oldItem: FilterItem, newItem: FilterItem): Any? {
|
|
||||||
val hasPayload = when {
|
|
||||||
oldItem is FilterItem.Tag && newItem is FilterItem.Tag -> {
|
|
||||||
oldItem.isChecked != newItem.isChecked
|
|
||||||
}
|
|
||||||
oldItem is FilterItem.Sort && newItem is FilterItem.Sort -> {
|
|
||||||
oldItem.isSelected != newItem.isSelected
|
|
||||||
}
|
|
||||||
oldItem is FilterItem.Header && newItem is FilterItem.Header -> {
|
|
||||||
oldItem.counter != newItem.counter
|
|
||||||
}
|
|
||||||
else -> false
|
|
||||||
}
|
|
||||||
return if (hasPayload) Unit else super.getChangePayload(oldItem, newItem)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,29 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.list.ui.filter
|
|
||||||
|
|
||||||
import androidx.annotation.StringRes
|
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaTag
|
|
||||||
import org.koitharu.kotatsu.parsers.model.SortOrder
|
|
||||||
|
|
||||||
sealed interface FilterItem {
|
|
||||||
|
|
||||||
class Header(
|
|
||||||
@StringRes val titleResId: Int,
|
|
||||||
val counter: Int,
|
|
||||||
) : FilterItem
|
|
||||||
|
|
||||||
class Sort(
|
|
||||||
val order: SortOrder,
|
|
||||||
val isSelected: Boolean,
|
|
||||||
) : FilterItem
|
|
||||||
|
|
||||||
class Tag(
|
|
||||||
val tag: MangaTag,
|
|
||||||
val isChecked: Boolean,
|
|
||||||
) : FilterItem
|
|
||||||
|
|
||||||
object Loading : FilterItem
|
|
||||||
|
|
||||||
class Error(
|
|
||||||
@StringRes val textResId: Int,
|
|
||||||
) : FilterItem
|
|
||||||
}
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue