Refactor image loading

master
Koitharu 1 year ago
parent bd4fecc3b6
commit 10bd46f077
Signed by: Koitharu
GPG Key ID: 676DEE768C17A9D7

@ -21,16 +21,11 @@ import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.model.getTitle
import org.koitharu.kotatsu.core.parser.favicon.faviconUri
import org.koitharu.kotatsu.core.ui.image.ChipIconTarget
import org.koitharu.kotatsu.core.ui.image.CoverSizeResolver
import org.koitharu.kotatsu.core.ui.image.TrimTransformation
import org.koitharu.kotatsu.core.ui.list.AdapterDelegateClickListenerAdapter
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.util.ext.defaultPlaceholders
import org.koitharu.kotatsu.core.util.ext.enqueueWith
import org.koitharu.kotatsu.core.util.ext.getQuantityStringSafe
import org.koitharu.kotatsu.core.util.ext.mangaExtra
import org.koitharu.kotatsu.core.util.ext.mangaSourceExtra
import org.koitharu.kotatsu.core.util.ext.newImageRequest
import org.koitharu.kotatsu.databinding.ItemMangaAlternativeBinding
import org.koitharu.kotatsu.list.ui.ListModelDiffCallback
import org.koitharu.kotatsu.list.ui.model.ListModel
@ -104,13 +99,6 @@ fun alternativeAD(
.allowRgb565(true)
.enqueueWith(coil)
}
binding.imageViewCover.newImageRequest(lifecycleOwner, item.manga.coverUrl)?.run {
size(CoverSizeResolver(binding.imageViewCover))
defaultPlaceholders(context)
transformations(TrimTransformation())
allowRgb565(true)
mangaExtra(item.manga)
enqueueWith(coil)
}
binding.imageViewCover.setImageAsync(item.manga.coverUrl, item.manga)
}
}

@ -51,7 +51,7 @@ class AlternativesActivity : BaseActivity<ActivityAlternativesBinding>(),
}
val listAdapter = BaseListAdapter<ListModel>()
.addDelegate(ListItemType.MANGA_LIST_DETAILED, alternativeAD(coil, this, this))
.addDelegate(ListItemType.STATE_EMPTY, emptyStateListAD(coil, this, null))
.addDelegate(ListItemType.STATE_EMPTY, emptyStateListAD(null))
.addDelegate(ListItemType.FOOTER_LOADING, loadingFooterAD())
.addDelegate(ListItemType.STATE_LOADING, loadingStateAD())
.addDelegate(ListItemType.FOOTER_BUTTON, buttonFooterAD(this))

@ -12,7 +12,6 @@ import androidx.appcompat.view.ActionMode
import androidx.core.view.WindowInsetsCompat
import androidx.fragment.app.viewModels
import androidx.recyclerview.widget.GridLayoutManager
import coil3.ImageLoader
import dagger.hilt.android.AndroidEntryPoint
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.bookmarks.domain.Bookmark
@ -50,9 +49,6 @@ class AllBookmarksFragment :
ListSelectionController.Callback,
FastScroller.FastScrollListener, ListHeaderClickListener {
@Inject
lateinit var coil: ImageLoader
@Inject
lateinit var settings: AppSettings
@ -79,8 +75,6 @@ class AllBookmarksFragment :
callback = this,
)
bookmarksAdapter = BookmarksAdapter(
lifecycleOwner = viewLifecycleOwner,
coil = coil,
clickListener = this,
headerClickListener = this,
)

@ -1,24 +1,13 @@
package org.koitharu.kotatsu.bookmarks.ui.adapter
import androidx.lifecycle.LifecycleOwner
import coil3.ImageLoader
import coil3.request.allowRgb565
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
import org.koitharu.kotatsu.bookmarks.domain.Bookmark
import org.koitharu.kotatsu.core.ui.image.CoverSizeResolver
import org.koitharu.kotatsu.core.ui.list.AdapterDelegateClickListenerAdapter
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.util.ext.bookmarkExtra
import org.koitharu.kotatsu.core.util.ext.decodeRegion
import org.koitharu.kotatsu.core.util.ext.defaultPlaceholders
import org.koitharu.kotatsu.core.util.ext.enqueueWith
import org.koitharu.kotatsu.core.util.ext.newImageRequest
import org.koitharu.kotatsu.databinding.ItemBookmarkLargeBinding
import org.koitharu.kotatsu.list.ui.model.ListModel
fun bookmarkLargeAD(
coil: ImageLoader,
lifecycleOwner: LifecycleOwner,
clickListener: OnListItemClickListener<Bookmark>,
) = adapterDelegateViewBinding<Bookmark, ListModel, ItemBookmarkLargeBinding>(
{ inflater, parent -> ItemBookmarkLargeBinding.inflate(inflater, parent, false) },
@ -26,14 +15,7 @@ fun bookmarkLargeAD(
AdapterDelegateClickListenerAdapter(this, clickListener).attach(itemView)
bind {
binding.imageViewThumb.newImageRequest(lifecycleOwner, item.imageLoadData)?.run {
size(CoverSizeResolver(binding.imageViewThumb))
defaultPlaceholders(context)
allowRgb565(true)
bookmarkExtra(item)
decodeRegion(item.scroll)
enqueueWith(coil)
}
binding.imageViewThumb.setImageAsync(item)
binding.progressView.setProgress(item.percent, false)
}
}

@ -1,8 +1,6 @@
package org.koitharu.kotatsu.bookmarks.ui.adapter
import android.content.Context
import androidx.lifecycle.LifecycleOwner
import coil3.ImageLoader
import org.koitharu.kotatsu.bookmarks.domain.Bookmark
import org.koitharu.kotatsu.core.ui.BaseListAdapter
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
@ -17,19 +15,17 @@ import org.koitharu.kotatsu.list.ui.adapter.loadingStateAD
import org.koitharu.kotatsu.list.ui.model.ListModel
class BookmarksAdapter(
coil: ImageLoader,
lifecycleOwner: LifecycleOwner,
clickListener: OnListItemClickListener<Bookmark>,
headerClickListener: ListHeaderClickListener?,
) : BaseListAdapter<ListModel>(), FastScroller.SectionIndexer {
init {
addDelegate(ListItemType.PAGE_THUMB, bookmarkLargeAD(coil, lifecycleOwner, clickListener))
addDelegate(ListItemType.PAGE_THUMB, bookmarkLargeAD(clickListener))
addDelegate(ListItemType.HEADER, listHeaderAD(headerClickListener))
addDelegate(ListItemType.STATE_ERROR, errorStateListAD(null))
addDelegate(ListItemType.FOOTER_LOADING, loadingFooterAD())
addDelegate(ListItemType.STATE_LOADING, loadingStateAD())
addDelegate(ListItemType.STATE_EMPTY, emptyStateListAD(coil, lifecycleOwner, null))
addDelegate(ListItemType.STATE_EMPTY, emptyStateListAD(null))
}
override fun getSectionText(context: Context, position: Int): CharSequence? {

@ -0,0 +1,192 @@
package org.koitharu.kotatsu.core.image
import android.content.Context
import android.graphics.drawable.Drawable
import android.util.AttributeSet
import androidx.annotation.AttrRes
import androidx.annotation.DrawableRes
import androidx.core.content.withStyledAttributes
import androidx.lifecycle.findViewTreeLifecycleOwner
import coil3.ImageLoader
import coil3.asImage
import coil3.request.Disposable
import coil3.request.ErrorResult
import coil3.request.ImageRequest
import coil3.request.NullRequestData
import coil3.request.SuccessResult
import coil3.request.allowRgb565
import coil3.request.crossfade
import coil3.request.lifecycle
import coil3.request.target
import coil3.size.Scale
import coil3.size.Size
import coil3.size.SizeResolver
import coil3.size.ViewSizeResolver
import coil3.util.CoilUtils
import com.google.android.material.imageview.ShapeableImageView
import dagger.hilt.android.AndroidEntryPoint
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.util.ext.decodeRegion
import org.koitharu.kotatsu.core.util.ext.getAnimationDuration
import org.koitharu.kotatsu.core.util.ext.isAnimationsEnabled
import java.util.LinkedList
import javax.inject.Inject
@AndroidEntryPoint
open class CoilImageView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
@AttrRes defStyleAttr: Int = 0,
) : ShapeableImageView(context, attrs, defStyleAttr), ImageRequest.Listener {
@Inject
lateinit var coil: ImageLoader
var allowRgb565: Boolean = false
var useExistingDrawable: Boolean = false
var decodeRegion: Boolean = false
var exactImageSize: Size? = null
var crossfadeDurationFactor: Float = 1f
var placeholderDrawable: Drawable? = null
var errorDrawable: Drawable? = null
var fallbackDrawable: Drawable? = null
private var currentRequest: Disposable? = null
private var currentImageData: Any = NullRequestData
private var listeners: MutableList<ImageRequest.Listener>? = null
init {
context.withStyledAttributes(attrs, R.styleable.CoilImageView, defStyleAttr) {
allowRgb565 = getBoolean(R.styleable.CoilImageView_allowRgb565, allowRgb565)
useExistingDrawable = getBoolean(R.styleable.CoilImageView_useExistingDrawable, useExistingDrawable)
decodeRegion = getBoolean(R.styleable.CoilImageView_decodeRegion, decodeRegion)
placeholderDrawable = getDrawable(R.styleable.CoilImageView_placeholderDrawable)
errorDrawable = getDrawable(R.styleable.CoilImageView_errorDrawable)
fallbackDrawable = getDrawable(R.styleable.CoilImageView_fallbackDrawable)
crossfadeDurationFactor = if (getBoolean(R.styleable.CoilImageView_crossfadeEnabled, true)) {
crossfadeDurationFactor
} else {
0f
}
}
}
override fun onCancel(request: ImageRequest) {
super.onCancel(request)
listeners?.forEach { it.onCancel(request) }
}
override fun onError(request: ImageRequest, result: ErrorResult) {
super.onError(request, result)
listeners?.forEach { it.onError(request, result) }
}
override fun onStart(request: ImageRequest) {
super.onStart(request)
listeners?.forEach { it.onStart(request) }
}
override fun onSuccess(request: ImageRequest, result: SuccessResult) {
super.onSuccess(request, result)
listeners?.forEach { it.onSuccess(request, result) }
}
fun addImageRequestListener(listener: ImageRequest.Listener) {
val list = listeners ?: LinkedList<ImageRequest.Listener>().also { listeners = it }
list.add(listener)
}
fun removeImageRequestListener(listener: ImageRequest.Listener) {
listeners?.remove(listener)
}
fun setImageAsync(@DrawableRes resourceId: Int) = enqueueRequest(
newRequestBuilder()
.data(resourceId)
.build(),
)
fun setImageAsync(url: String?) = enqueueRequest(
newRequestBuilder()
.data(url)
.build(),
)
@Deprecated("Use more specific overrides instead")
fun setImageAsync(request: ImageRequest) = enqueueRequest(
request.newBuilder()
.lifecycle(request.lifecycle ?: findViewTreeLifecycleOwner()?.lifecycle)
.target(this)
.size(
if (request.sizeResolver == SizeResolver.ORIGINAL) {
ViewSizeResolver(this)
} else {
request.sizeResolver
},
).build(),
)
fun disposeImage() {
CoilUtils.dispose(this)
currentRequest = null
currentImageData = NullRequestData
setImageDrawable(null)
}
protected fun enqueueRequest(request: ImageRequest): Disposable {
val previous = currentRequest
if (currentImageData == request.data && previous?.job?.isCancelled == false) {
return previous
}
currentImageData = request.data
return coil.enqueue(request).also { currentRequest = it }
}
protected open fun newRequestBuilder() = ImageRequest.Builder(context).apply {
lifecycle(findViewTreeLifecycleOwner())
val crossfadeDuration = if (context.isAnimationsEnabled) {
(context.getAnimationDuration(R.integer.config_defaultAnimTime) * crossfadeDurationFactor).toInt()
} else {
0
}
crossfade(crossfadeDuration)
if (useExistingDrawable) {
val previousDrawable = this@CoilImageView.drawable?.asImage()
if (previousDrawable != null) {
fallback(previousDrawable)
placeholder(previousDrawable)
error(previousDrawable)
} else {
setupPlaceholders()
}
} else {
setupPlaceholders()
}
if (decodeRegion) {
decodeRegion(0)
}
size(
exactImageSize?.let {
SizeResolver(it)
} ?: ViewSizeResolver(this@CoilImageView),
)
scale(scaleType.toCoilScale())
listener(this@CoilImageView)
allowRgb565(allowRgb565)
target(this@CoilImageView)
}
private fun ImageRequest.Builder.setupPlaceholders() {
placeholder(placeholderDrawable?.asImage())
error(errorDrawable?.asImage())
fallback(fallbackDrawable?.asImage())
}
private fun ScaleType.toCoilScale(): Scale = if (this == ScaleType.CENTER_CROP) {
Scale.FILL
} else {
Scale.FIT
}
}

@ -44,12 +44,6 @@ class AnimatedFaviconDrawable(
super.draw(canvas)
}
// override fun setAlpha(alpha: Int) = Unit
//
// override fun getAlpha(): Int = 255
//
// override fun isOpaque(): Boolean = false
override fun onTimeUpdate(animation: TimeAnimator?, totalTime: Long, deltaTime: Long) {
callback?.also {
updateColor()

@ -1,105 +0,0 @@
package org.koitharu.kotatsu.core.ui.image
import android.view.ViewGroup
import android.view.ViewTreeObserver
import android.view.ViewTreeObserver.OnPreDrawListener
import android.widget.ImageView
import coil3.size.Dimension
import coil3.size.Size
import coil3.size.ViewSizeResolver
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlin.coroutines.resume
import kotlin.math.roundToInt
private const val ASPECT_RATIO_HEIGHT = 18f
private const val ASPECT_RATIO_WIDTH = 13f
class CoverSizeResolver(
override val view: ImageView,
) : ViewSizeResolver<ImageView> {
override suspend fun size(): Size {
// Fast path: the view is already measured.
getSize()?.let { return it }
// Slow path: wait for the view to be measured.
return suspendCancellableCoroutine { continuation ->
val viewTreeObserver = view.viewTreeObserver
val preDrawListener = object : OnPreDrawListener {
private var isResumed = false
override fun onPreDraw(): Boolean {
val size = getSize()
if (size != null) {
viewTreeObserver.removePreDrawListenerSafe(this)
if (!isResumed) {
isResumed = true
continuation.resume(size)
}
}
return true
}
}
viewTreeObserver.addOnPreDrawListener(preDrawListener)
continuation.invokeOnCancellation {
viewTreeObserver.removePreDrawListenerSafe(preDrawListener)
}
}
}
private fun getSize(): Size? {
var width = getWidth()
var height = getHeight()
when {
width == null && height == null -> {
return null
}
height == null && width != null -> {
height = Dimension((width.px * ASPECT_RATIO_HEIGHT / ASPECT_RATIO_WIDTH).roundToInt())
}
width == null && height != null -> {
width = Dimension((height.px * ASPECT_RATIO_WIDTH / ASPECT_RATIO_HEIGHT).roundToInt())
}
}
return Size(checkNotNull(width), checkNotNull(height))
}
private fun getWidth() = getDimension(
paramSize = view.layoutParams?.width ?: -1,
viewSize = view.width,
paddingSize = if (subtractPadding) view.paddingLeft + view.paddingRight else 0
)
private fun getHeight() = getDimension(
paramSize = view.layoutParams?.height ?: -1,
viewSize = view.height,
paddingSize = if (subtractPadding) view.paddingTop + view.paddingBottom else 0
)
private fun getDimension(paramSize: Int, viewSize: Int, paddingSize: Int): Dimension.Pixels? {
if (paramSize == ViewGroup.LayoutParams.WRAP_CONTENT) {
return null
}
val insetParamSize = paramSize - paddingSize
if (insetParamSize > 0) {
return Dimension(insetParamSize)
}
val insetViewSize = viewSize - paddingSize
if (insetViewSize > 0) {
return Dimension(insetViewSize)
}
return null
}
private fun ViewTreeObserver.removePreDrawListenerSafe(victim: OnPreDrawListener) {
if (isAlive) {
removeOnPreDrawListener(victim)
} else {
view.viewTreeObserver.removeOnPreDrawListener(victim)
}
}
}

@ -0,0 +1,64 @@
package org.koitharu.kotatsu.core.ui.image
import android.content.Context
import android.util.AttributeSet
import androidx.annotation.AttrRes
import androidx.annotation.StyleRes
import androidx.core.content.withStyledAttributes
import coil3.Image
import coil3.asImage
import coil3.request.Disposable
import coil3.request.ImageRequest
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.browser.cloudflare.CaptchaNotifier.Companion.ignoreCaptchaErrors
import org.koitharu.kotatsu.core.image.CoilImageView
import org.koitharu.kotatsu.core.parser.favicon.faviconUri
import org.koitharu.kotatsu.core.util.ext.isAnimationsEnabled
import org.koitharu.kotatsu.core.util.ext.mangaSourceExtra
import org.koitharu.kotatsu.parsers.model.MangaSource
class FaviconView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
@AttrRes defStyleAttr: Int = 0,
) : CoilImageView(context, attrs, defStyleAttr) {
@StyleRes
private var iconStyle: Int = R.style.FaviconDrawable
init {
context.withStyledAttributes(attrs, R.styleable.FaviconView, defStyleAttr) {
iconStyle = getResourceId(R.styleable.FaviconView_iconStyle, iconStyle)
}
if (isInEditMode) {
setImageDrawable(
FaviconDrawable(
context = context,
styleResId = iconStyle,
name = context.getString(R.string.app_name).random().toString(),
),
)
}
}
fun setImageAsync(mangaSource: MangaSource): Disposable {
val fallbackFactory: (ImageRequest) -> Image? = {
FaviconDrawable(context, iconStyle, mangaSource.name).asImage()
}
val placeholderFactory: (ImageRequest) -> Image? = if (context.isAnimationsEnabled) {
{ AnimatedFaviconDrawable(context, iconStyle, mangaSource.name).asImage() }
} else {
fallbackFactory
}
return enqueueRequest(
newRequestBuilder()
.data(mangaSource.faviconUri())
.error(fallbackFactory)
.fallback(fallbackFactory)
.placeholder(placeholderFactory)
.mangaSourceExtra(mangaSource)
.ignoreCaptchaErrors()
.build(),
)
}
}

@ -8,6 +8,7 @@ import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import androidx.core.content.ContextCompat
import androidx.core.view.children
import androidx.lifecycle.findViewTreeLifecycleOwner
import coil3.ImageLoader
import coil3.request.Disposable
import coil3.request.ImageRequest
@ -15,6 +16,7 @@ import coil3.request.allowRgb565
import coil3.request.crossfade
import coil3.request.error
import coil3.request.fallback
import coil3.request.lifecycle
import coil3.request.placeholder
import coil3.request.transformations
import coil3.transform.RoundedCornersTransformation
@ -203,6 +205,7 @@ class ChipsView @JvmOverloads constructor(
.target(ChipIconTarget(this))
.placeholder(placeholder)
.fallback(placeholder)
.lifecycle(this@ChipsView.findViewTreeLifecycleOwner())
.error(placeholder)
.transformations(RoundedCornersTransformation(resources.getDimension(R.dimen.chip_icon_corner)))
.allowRgb565(true)

@ -1,43 +0,0 @@
package org.koitharu.kotatsu.core.ui.widgets
import android.content.Context
import android.util.AttributeSet
import android.widget.LinearLayout.HORIZONTAL
import android.widget.LinearLayout.VERTICAL
import androidx.annotation.AttrRes
import androidx.core.content.withStyledAttributes
import com.google.android.material.imageview.ShapeableImageView
import org.koitharu.kotatsu.R
import kotlin.math.roundToInt
private const val ASPECT_RATIO_HEIGHT = 3f
private const val ASPECT_RATIO_WIDTH = 2f
class CoverImageView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
@AttrRes defStyleAttr: Int = 0,
) : ShapeableImageView(context, attrs, defStyleAttr) {
private var orientation: Int = HORIZONTAL
init {
context.withStyledAttributes(attrs, R.styleable.CoverImageView, defStyleAttr) {
orientation = getInt(R.styleable.CoverImageView_android_orientation, orientation)
}
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
val desiredWidth: Int
val desiredHeight: Int
if (orientation == VERTICAL) {
desiredHeight = measuredHeight
desiredWidth = (desiredHeight * ASPECT_RATIO_WIDTH / ASPECT_RATIO_HEIGHT).roundToInt()
} else {
desiredWidth = measuredWidth
desiredHeight = (desiredWidth * ASPECT_RATIO_HEIGHT / ASPECT_RATIO_WIDTH).roundToInt()
}
setMeasuredDimension(desiredWidth, desiredHeight)
}
}

@ -0,0 +1,85 @@
package org.koitharu.kotatsu.core.ui.widgets
import android.content.Context
import android.util.AttributeSet
import android.view.View
import android.view.ViewGroup
import androidx.annotation.AttrRes
import androidx.core.view.children
import androidx.core.view.isEmpty
import androidx.core.view.isGone
open class StackLayout @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
@AttrRes defStyleAttr: Int = 0,
) : ViewGroup(context, attrs, defStyleAttr) {
private val visibleChildren = ArrayList<View>()
override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
val w = r - l - paddingLeft - paddingRight
val h = b - t - paddingTop - paddingBottom
visibleChildren.clear()
children.filterNotTo(visibleChildren) { it.isGone }
if (w <= 0 || h <= 0 || visibleChildren.isEmpty) {
return
}
val xStep = w / (visibleChildren.size + 1)
val yStep = h / (visibleChildren.size + 1)
val maxW = w
val maxH = h
val total = visibleChildren.size
for ((index, child) in visibleChildren.withIndex()) {
var cx = paddingLeft + xStep * (total - index)
var cy = paddingTop + yStep * (index + 1)
val rx = child.measuredWidth.coerceAtMost(maxW) / 2
val ry = child.measuredHeight.coerceAtMost(maxH) / 2
if (cx < rx) {
cx = rx
}
if (cy < ry) {
cy = ry
}
if (cx + rx > width) {
cx = width - rx
}
if (cy + ry > height) {
cy = height - ry
}
child.layout(cx - rx, cy - ry, cx + rx, cy + ry)
}
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
measureChildren(widthMeasureSpec, heightMeasureSpec)
if (isEmpty()) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
return
}
var h = 0
var w = 0
for (i in 0 until childCount) {
val child = getChildAt(i)
if (child.isGone) {
continue
}
val mw = child.measuredWidth
val mh = child.measuredHeight
if (h == 0 || w == 0) {
h = mh
w = mw
} else {
h += mh / 2
w += mw / 2
}
}
h += paddingTop + paddingBottom
w += paddingLeft + paddingRight
setMeasuredDimension(
resolveSizeAndState(w, widthMeasureSpec, 0),
resolveSizeAndState(h, heightMeasureSpec, 0),
)
}
}

@ -1,11 +1,6 @@
package org.koitharu.kotatsu.core.util.ext
import android.content.Context
import android.graphics.drawable.Drawable
import android.widget.ImageView
import androidx.core.graphics.ColorUtils
import androidx.core.graphics.drawable.toDrawable
import androidx.lifecycle.LifecycleOwner
import coil3.Extras
import coil3.ImageLoader
import coil3.asDrawable
@ -16,48 +11,11 @@ import coil3.request.ImageResult
import coil3.request.Options
import coil3.request.SuccessResult
import coil3.request.bitmapConfig
import coil3.request.crossfade
import coil3.request.error
import coil3.request.fallback
import coil3.request.lifecycle
import coil3.request.placeholder
import coil3.request.target
import coil3.size.Scale
import coil3.size.ViewSizeResolver
import coil3.toBitmap
import coil3.util.CoilUtils
import com.google.android.material.progressindicator.BaseProgressIndicator
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.bookmarks.domain.Bookmark
import org.koitharu.kotatsu.core.image.RegionBitmapDecoder
import org.koitharu.kotatsu.core.ui.image.AnimatedPlaceholderDrawable
import org.koitharu.kotatsu.core.util.progress.ImageRequestIndicatorListener
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaSource
import androidx.appcompat.R as appcompatR
import com.google.android.material.R as materialR
fun ImageView.newImageRequest(lifecycleOwner: LifecycleOwner, data: Any?): ImageRequest.Builder? {
val current = CoilUtils.result(this)
if (current?.request?.lifecycle === lifecycleOwner.lifecycle) {
if (current is SuccessResult && current.request.data == data) {
return null
}
}
// disposeImageRequest()
return ImageRequest.Builder(context)
.data(data?.takeUnless { it == "" || it == 0 })
.lifecycle(lifecycleOwner)
.crossfade(context)
.size(ViewSizeResolver(this))
.scale(scaleType.toCoilScale())
.target(this)
}
fun ImageView.disposeImageRequest() {
CoilUtils.dispose(this)
setImageDrawable(null)
}
fun ImageRequest.Builder.enqueueWith(loader: ImageLoader) = loader.enqueue(build())
@ -79,10 +37,6 @@ fun ImageResult.toBitmapOrNull() = when (this) {
is ErrorResult -> null
}
fun ImageRequest.Builder.indicator(indicators: List<BaseProgressIndicator<*>>): ImageRequest.Builder {
return addListener(ImageRequestIndicatorListener(indicators))
}
fun ImageRequest.Builder.decodeRegion(
scroll: Int = RegionBitmapDecoder.SCROLL_UNDEFINED,
): ImageRequest.Builder = apply {
@ -90,19 +44,13 @@ fun ImageRequest.Builder.decodeRegion(
extras[RegionBitmapDecoder.regionScrollKey] = scroll
}
@Suppress("SpellCheckingInspection")
fun ImageRequest.Builder.crossfade(context: Context): ImageRequest.Builder {
val duration = context.resources.getInteger(R.integer.config_defaultAnimTime) * context.animatorDurationScale
return crossfade(duration.toInt())
}
fun ImageRequest.Builder.mangaSourceExtra(source: MangaSource?): ImageRequest.Builder = apply {
extras[mangaSourceKey] = source
}
fun ImageRequest.Builder.mangaExtra(manga: Manga): ImageRequest.Builder = apply {
fun ImageRequest.Builder.mangaExtra(manga: Manga?): ImageRequest.Builder = apply {
extras[mangaKey] = manga
mangaSourceExtra(manga.source)
mangaSourceExtra(manga?.source)
}
fun ImageRequest.Builder.bookmarkExtra(bookmark: Bookmark): ImageRequest.Builder = apply {
@ -110,56 +58,12 @@ fun ImageRequest.Builder.bookmarkExtra(bookmark: Bookmark): ImageRequest.Builder
mangaSourceExtra(bookmark.manga.source)
}
fun ImageRequest.Builder.defaultPlaceholders(context: Context): ImageRequest.Builder {
val errorColor = ColorUtils.blendARGB(
context.getThemeColor(materialR.attr.colorErrorContainer),
context.getThemeColor(appcompatR.attr.colorBackgroundFloating),
0.25f,
)
return placeholder(AnimatedPlaceholderDrawable(context))
.fallback(context.getThemeColor(materialR.attr.colorSurfaceContainer).toDrawable())
.error(errorColor.toDrawable())
}
private fun ImageView.ScaleType.toCoilScale(): Scale = if (this == ImageView.ScaleType.CENTER_CROP) {
Scale.FILL
} else {
Scale.FIT
}
fun ImageRequest.Builder.addListener(listener: ImageRequest.Listener): ImageRequest.Builder {
val existing = build().listener
return listener(
when (existing) {
null -> listener
is CompositeImageRequestListener -> existing + listener
else -> CompositeImageRequestListener(arrayOf(existing, listener))
},
)
}
suspend fun ImageLoader.fetch(data: Any, options: Options): FetchResult? {
val mappedData = components.map(data, options)
val fetcher = components.newFetcher(mappedData, options, this)?.first
return fetcher?.fetch()
}
private class CompositeImageRequestListener(
private val delegates: Array<ImageRequest.Listener>,
) : ImageRequest.Listener {
override fun onCancel(request: ImageRequest) = delegates.forEach { it.onCancel(request) }
override fun onError(request: ImageRequest, result: ErrorResult) = delegates.forEach { it.onError(request, result) }
override fun onStart(request: ImageRequest) = delegates.forEach { it.onStart(request) }
override fun onSuccess(request: ImageRequest, result: SuccessResult) =
delegates.forEach { it.onSuccess(request, result) }
operator fun plus(other: ImageRequest.Listener) = CompositeImageRequestListener(delegates + other)
}
val mangaKey = Extras.Key<Manga?>(null)
val bookmarkKey = Extras.Key<Bookmark?>(null)
val mangaSourceKey = Extras.Key<MangaSource?>(null)

@ -76,6 +76,7 @@ fun Context.getThemeResId(
it.getResourceId(0, fallback)
}
@Deprecated("")
fun TypedArray.getDrawableCompat(context: Context, index: Int): Drawable? {
val resId = getResourceId(index, 0)
return if (resId != 0) ContextCompat.getDrawable(context, resId) else null

@ -22,19 +22,12 @@ import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import androidx.transition.TransitionManager
import coil3.ImageLoader
import coil3.request.ImageRequest
import coil3.request.SuccessResult
import coil3.request.allowRgb565
import coil3.request.crossfade
import coil3.request.error
import coil3.request.fallback
import coil3.request.lifecycle
import coil3.request.placeholder
import coil3.request.target
import coil3.request.transformations
import coil3.size.Precision
import coil3.size.Scale
import coil3.transform.RoundedCornersTransformation
import coil3.util.CoilUtils
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.chip.Chip
import dagger.hilt.android.AndroidEntryPoint
@ -59,7 +52,6 @@ import org.koitharu.kotatsu.core.parser.favicon.faviconUri
import org.koitharu.kotatsu.core.ui.BaseActivity
import org.koitharu.kotatsu.core.ui.BaseListAdapter
import org.koitharu.kotatsu.core.ui.dialog.buildAlertDialog
import org.koitharu.kotatsu.core.ui.image.CoverSizeResolver
import org.koitharu.kotatsu.core.ui.image.FaviconDrawable
import org.koitharu.kotatsu.core.ui.image.TextDrawable
import org.koitharu.kotatsu.core.ui.image.TextViewTarget
@ -72,9 +64,6 @@ import org.koitharu.kotatsu.core.util.FileSize
import org.koitharu.kotatsu.core.util.LocaleUtils
import org.koitharu.kotatsu.core.util.ext.consume
import org.koitharu.kotatsu.core.util.ext.copyToClipboard
import org.koitharu.kotatsu.core.util.ext.crossfade
import org.koitharu.kotatsu.core.util.ext.defaultPlaceholders
import org.koitharu.kotatsu.core.util.ext.drawable
import org.koitharu.kotatsu.core.util.ext.drawableStart
import org.koitharu.kotatsu.core.util.ext.end
import org.koitharu.kotatsu.core.util.ext.enqueueWith
@ -369,8 +358,7 @@ class DetailsActivity :
.addDelegate(
ListItemType.MANGA_GRID,
mangaGridItemAD(
coil, this,
StaticItemSizeResolver(resources.getDimensionPixelSize(R.dimen.smaller_grid_width)),
sizeResolver = StaticItemSizeResolver(resources.getDimensionPixelSize(R.dimen.smaller_grid_width)),
) { item, view ->
router.openDetails(item)
},
@ -505,28 +493,7 @@ class DetailsActivity :
}
private fun loadCover(imageUrl: String?) {
viewBinding.imageViewCover.isEnabled = !imageUrl.isNullOrEmpty()
val lastResult = CoilUtils.result(viewBinding.imageViewCover)
if (lastResult is SuccessResult && lastResult.request.data == imageUrl) {
return
}
val request = ImageRequest.Builder(this)
.target(viewBinding.imageViewCover)
.size(CoverSizeResolver(viewBinding.imageViewCover))
.scale(Scale.FILL)
.data(imageUrl)
.mangaSourceExtra(viewModel.getMangaOrNull()?.source)
.crossfade(this)
.lifecycle(this)
val previousDrawable = lastResult?.drawable
if (previousDrawable != null) {
request.fallback(previousDrawable)
.placeholder(previousDrawable)
.error(previousDrawable)
} else {
request.defaultPlaceholders(this)
}
request.enqueueWith(coil)
viewBinding.imageViewCover.setImageAsync(imageUrl, viewModel.getMangaOrNull())
}
private fun String.withEstimatedTime(time: ReadingTime?): String {

@ -12,7 +12,6 @@ import androidx.core.view.WindowInsetsCompat
import androidx.fragment.app.viewModels
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView
import coil3.ImageLoader
import dagger.hilt.android.AndroidEntryPoint
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.bookmarks.domain.Bookmark
@ -52,9 +51,6 @@ class BookmarksFragment : BaseFragment<FragmentMangaBookmarksBinding>(),
private val activityViewModel by ChaptersPagesViewModel.ActivityVMLazy(this)
private val viewModel by viewModels<BookmarksViewModel>()
@Inject
lateinit var coil: ImageLoader
@Inject
lateinit var settings: AppSettings
@ -89,8 +85,6 @@ class BookmarksFragment : BaseFragment<FragmentMangaBookmarksBinding>(),
callback = this,
)
bookmarksAdapter = BookmarksAdapter(
coil = coil,
lifecycleOwner = viewLifecycleOwner,
clickListener = this@BookmarksFragment,
headerClickListener = null,
)

@ -1,37 +1,23 @@
package org.koitharu.kotatsu.details.ui.pager.pages
import androidx.lifecycle.LifecycleOwner
import coil3.ImageLoader
import coil3.request.allowRgb565
import coil3.request.transformations
import coil3.size.Scale
import coil3.size.Size
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.ui.image.TrimTransformation
import org.koitharu.kotatsu.core.ui.list.AdapterDelegateClickListenerAdapter
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.util.ext.decodeRegion
import org.koitharu.kotatsu.core.util.ext.defaultPlaceholders
import org.koitharu.kotatsu.core.util.ext.enqueueWith
import org.koitharu.kotatsu.core.util.ext.mangaSourceExtra
import org.koitharu.kotatsu.core.util.ext.newImageRequest
import org.koitharu.kotatsu.core.util.ext.setTextColorAttr
import org.koitharu.kotatsu.databinding.ItemPageThumbBinding
import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.parsers.util.nullIfEmpty
import com.google.android.material.R as materialR
fun pageThumbnailAD(
coil: ImageLoader,
lifecycleOwner: LifecycleOwner,
clickListener: OnListItemClickListener<PageThumbnail>,
) = adapterDelegateViewBinding<PageThumbnail, ListModel, ItemPageThumbBinding>(
{ inflater, parent -> ItemPageThumbBinding.inflate(inflater, parent, false) },
) {
val gridWidth = itemView.context.resources.getDimensionPixelSize(R.dimen.preferred_grid_width)
val thumbSize = Size(
binding.imageViewThumb.exactImageSize = Size(
width = gridWidth,
height = (gridWidth / 13f * 18f).toInt(),
)
@ -39,17 +25,7 @@ fun pageThumbnailAD(
AdapterDelegateClickListenerAdapter(this, clickListener).attach(itemView)
bind {
val data: Any = item.page.preview?.nullIfEmpty() ?: item.page.toMangaPage()
binding.imageViewThumb.newImageRequest(lifecycleOwner, data)?.run {
defaultPlaceholders(context)
size(thumbSize)
scale(Scale.FILL)
allowRgb565(true)
transformations(TrimTransformation())
decodeRegion(0)
mangaSourceExtra(item.page.source)
enqueueWith(coil)
}
binding.imageViewThumb.setImageAsync(item.page)
with(binding.textViewNumber) {
setBackgroundResource(if (item.isCurrent) R.drawable.bg_badge_accent else R.drawable.bg_badge_empty)
setTextColorAttr(if (item.isCurrent) materialR.attr.colorOnTertiary else android.R.attr.textColorPrimary)

@ -17,7 +17,7 @@ class PageThumbnailAdapter(
) : BaseListAdapter<ListModel>(), FastScroller.SectionIndexer {
init {
addDelegate(ListItemType.PAGE_THUMB, pageThumbnailAD(coil, lifecycleOwner, clickListener))
addDelegate(ListItemType.PAGE_THUMB, pageThumbnailAD(clickListener))
addDelegate(ListItemType.HEADER, listHeaderAD(null))
}

@ -1,20 +1,13 @@
package org.koitharu.kotatsu.details.ui.scrobbling
import androidx.lifecycle.LifecycleOwner
import coil3.ImageLoader
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.nav.AppRouter
import org.koitharu.kotatsu.core.util.ext.defaultPlaceholders
import org.koitharu.kotatsu.core.util.ext.enqueueWith
import org.koitharu.kotatsu.core.util.ext.newImageRequest
import org.koitharu.kotatsu.databinding.ItemScrobblingInfoBinding
import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblingInfo
fun scrobblingInfoAD(
lifecycleOwner: LifecycleOwner,
coil: ImageLoader,
router: AppRouter,
) = adapterDelegateViewBinding<ScrobblingInfo, ListModel, ItemScrobblingInfoBinding>(
{ layoutInflater, parent -> ItemScrobblingInfoBinding.inflate(layoutInflater, parent, false) },
@ -24,10 +17,7 @@ fun scrobblingInfoAD(
}
bind {
binding.imageViewCover.newImageRequest(lifecycleOwner, item.coverUrl)?.run {
defaultPlaceholders(context)
enqueueWith(coil)
}
binding.imageViewCover.setImageAsync(item.coverUrl)
binding.textViewTitle.setText(item.scrobbler.titleResId)
binding.imageViewIcon.setImageResource(item.scrobbler.iconResId)
binding.ratingBar.rating = item.rating * binding.ratingBar.numStars

@ -21,10 +21,7 @@ import org.koitharu.kotatsu.core.nav.AppRouter
import org.koitharu.kotatsu.core.nav.router
import org.koitharu.kotatsu.core.ui.sheet.BaseAdaptiveSheet
import org.koitharu.kotatsu.core.util.ext.consume
import org.koitharu.kotatsu.core.util.ext.defaultPlaceholders
import org.koitharu.kotatsu.core.util.ext.enqueueWith
import org.koitharu.kotatsu.core.util.ext.getDisplayMessage
import org.koitharu.kotatsu.core.util.ext.newImageRequest
import org.koitharu.kotatsu.core.util.ext.observe
import org.koitharu.kotatsu.core.util.ext.observeEvent
import org.koitharu.kotatsu.core.util.ext.sanitize
@ -137,10 +134,7 @@ class ScrobblingInfoSheet :
binding.spinnerStatus.setSelection(scrobbling.status?.ordinal ?: -1)
binding.imageViewLogo.contentDescription = getString(scrobbling.scrobbler.titleResId)
binding.imageViewLogo.setImageResource(scrobbling.scrobbler.iconResId)
binding.imageViewCover.newImageRequest(viewLifecycleOwner, scrobbling.coverUrl)?.apply {
defaultPlaceholders(binding.imageViewCover.context)
enqueueWith(coil)
}
binding.imageViewCover.setImageAsync(scrobbling.coverUrl)
}
override fun onMenuItemClick(item: MenuItem): Boolean {

@ -13,6 +13,6 @@ class ScrollingInfoAdapter(
) : BaseListAdapter<ListModel>() {
init {
delegatesManager.addDelegate(scrobblingInfoAD(lifecycleOwner, coil, router))
delegatesManager.addDelegate(scrobblingInfoAD(router))
}
}

@ -7,23 +7,13 @@ import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.work.WorkInfo
import coil3.ImageLoader
import coil3.request.SuccessResult
import coil3.request.allowRgb565
import coil3.request.transformations
import coil3.util.CoilUtils
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
import kotlinx.coroutines.CoroutineStart
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.ui.BaseListAdapter
import org.koitharu.kotatsu.core.ui.image.TrimTransformation
import org.koitharu.kotatsu.core.util.ext.defaultPlaceholders
import org.koitharu.kotatsu.core.util.ext.enqueueWith
import org.koitharu.kotatsu.core.util.ext.getQuantityStringSafe
import org.koitharu.kotatsu.core.util.ext.mangaSourceExtra
import org.koitharu.kotatsu.core.util.ext.newImageRequest
import org.koitharu.kotatsu.core.util.ext.textAndVisible
import org.koitharu.kotatsu.databinding.ItemDownloadBinding
import org.koitharu.kotatsu.download.ui.list.chapters.DownloadChapter
@ -35,7 +25,6 @@ import org.koitharu.kotatsu.parsers.util.format
fun downloadItemAD(
lifecycleOwner: LifecycleOwner,
coil: ImageLoader,
listener: DownloadItemListener,
) = adapterDelegateViewBinding<DownloadItemModel, ListModel, ItemDownloadBinding>(
{ inflater, parent -> ItemDownloadBinding.inflate(inflater, parent, false) },
@ -89,16 +78,7 @@ fun downloadItemAD(
bind { payloads ->
binding.textViewTitle.text = item.manga?.title ?: getString(R.string.unknown)
if ((CoilUtils.result(binding.imageViewCover) as? SuccessResult)?.memoryCacheKey != item.coverCacheKey) {
binding.imageViewCover.newImageRequest(lifecycleOwner, item.manga?.coverUrl)?.apply {
defaultPlaceholders(context)
allowRgb565(true)
transformations(TrimTransformation())
memoryCacheKey(item.coverCacheKey)
mangaSourceExtra(item.manga?.source)
enqueueWith(coil)
}
}
binding.imageViewCover.setImageAsync(item.manga?.coverUrl, item.manga)
if (chaptersJob == null || payloads.isEmpty()) {
chaptersJob?.cancel()
chaptersJob = lifecycleOwner.lifecycleScope.launch(start = CoroutineStart.UNDISPATCHED) {

@ -44,7 +44,7 @@ class DownloadsActivity : BaseActivity<ActivityDownloadsBinding>(),
super.onCreate(savedInstanceState)
setContentView(ActivityDownloadsBinding.inflate(layoutInflater))
setDisplayHomeAsUp(true, false)
val downloadsAdapter = DownloadsAdapter(this, coil, this)
val downloadsAdapter = DownloadsAdapter(this, this)
val decoration = TypedListSpacingDecoration(this, false)
selectionController = ListSelectionController(
appCompatDelegate = delegate,

@ -1,7 +1,6 @@
package org.koitharu.kotatsu.download.ui.list
import androidx.lifecycle.LifecycleOwner
import coil3.ImageLoader
import org.koitharu.kotatsu.core.ui.BaseListAdapter
import org.koitharu.kotatsu.list.ui.adapter.ListItemType
import org.koitharu.kotatsu.list.ui.adapter.emptyStateListAD
@ -11,14 +10,13 @@ import org.koitharu.kotatsu.list.ui.model.ListModel
class DownloadsAdapter(
lifecycleOwner: LifecycleOwner,
coil: ImageLoader,
listener: DownloadItemListener,
) : BaseListAdapter<ListModel>() {
init {
addDelegate(ListItemType.DOWNLOAD, downloadItemAD(lifecycleOwner, coil, listener))
addDelegate(ListItemType.DOWNLOAD, downloadItemAD(lifecycleOwner, listener))
addDelegate(ListItemType.STATE_LOADING, loadingStateAD())
addDelegate(ListItemType.STATE_EMPTY, emptyStateListAD(coil, lifecycleOwner, null))
addDelegate(ListItemType.STATE_EMPTY, emptyStateListAD(null))
addDelegate(ListItemType.HEADER, listHeaderAD(null))
}
}

@ -17,7 +17,6 @@ import androidx.fragment.app.viewModels
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import coil3.ImageLoader
import dagger.hilt.android.AndroidEntryPoint
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.exceptions.resolve.SnackbarErrorObserver
@ -45,7 +44,6 @@ import org.koitharu.kotatsu.list.ui.adapter.TypedListSpacingDecoration
import org.koitharu.kotatsu.list.ui.model.ListHeader
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaParserSource
import javax.inject.Inject
@AndroidEntryPoint
class ExploreFragment :
@ -54,9 +52,6 @@ class ExploreFragment :
ExploreListEventListener,
OnListItemClickListener<MangaSourceItem>, ListSelectionController.Callback {
@Inject
lateinit var coil: ImageLoader
private val viewModel by viewModels<ExploreViewModel>()
private var exploreAdapter: ExploreAdapter? = null
private var sourceSelectionController: ListSelectionController? = null
@ -70,7 +65,7 @@ class ExploreFragment :
override fun onViewBindingCreated(binding: FragmentExploreBinding, savedInstanceState: Bundle?) {
super.onViewBindingCreated(binding, savedInstanceState)
exploreAdapter = ExploreAdapter(coil, viewLifecycleOwner, this, this) { manga, view ->
exploreAdapter = ExploreAdapter(this, this) { manga, view ->
router.openDetails(manga)
}
sourceSelectionController = ListSelectionController(

@ -1,7 +1,5 @@
package org.koitharu.kotatsu.explore.ui.adapter
import androidx.lifecycle.LifecycleOwner
import coil3.ImageLoader
import org.koitharu.kotatsu.core.ui.BaseListAdapter
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.explore.ui.model.MangaSourceItem
@ -13,8 +11,6 @@ import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.parsers.model.Manga
class ExploreAdapter(
coil: ImageLoader,
lifecycleOwner: LifecycleOwner,
listener: ExploreListEventListener,
clickListener: OnListItemClickListener<MangaSourceItem>,
mangaClickListener: OnListItemClickListener<Manga>,
@ -24,12 +20,12 @@ class ExploreAdapter(
addDelegate(ListItemType.EXPLORE_BUTTONS, exploreButtonsAD(listener))
addDelegate(
ListItemType.EXPLORE_SUGGESTION,
exploreRecommendationItemAD(coil, mangaClickListener, lifecycleOwner),
exploreRecommendationItemAD(mangaClickListener),
)
addDelegate(ListItemType.HEADER, listHeaderAD(listener))
addDelegate(ListItemType.EXPLORE_SOURCE_LIST, exploreSourceListItemAD(coil, clickListener, lifecycleOwner))
addDelegate(ListItemType.EXPLORE_SOURCE_GRID, exploreSourceGridItemAD(coil, clickListener, lifecycleOwner))
addDelegate(ListItemType.HINT_EMPTY, emptyHintAD(coil, lifecycleOwner, listener))
addDelegate(ListItemType.EXPLORE_SOURCE_LIST, exploreSourceListItemAD(clickListener))
addDelegate(ListItemType.EXPLORE_SOURCE_GRID, exploreSourceGridItemAD(clickListener))
addDelegate(ListItemType.HINT_EMPTY, emptyHintAD(listener))
addDelegate(ListItemType.STATE_LOADING, loadingStateAD())
}
}

@ -2,29 +2,14 @@ package org.koitharu.kotatsu.explore.ui.adapter
import android.view.View
import androidx.core.content.ContextCompat
import androidx.lifecycle.LifecycleOwner
import coil3.ImageLoader
import coil3.request.allowRgb565
import coil3.request.error
import coil3.request.fallback
import coil3.request.placeholder
import coil3.request.transformations
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.model.getSummary
import org.koitharu.kotatsu.core.model.getTitle
import org.koitharu.kotatsu.core.parser.favicon.faviconUri
import org.koitharu.kotatsu.core.ui.BaseListAdapter
import org.koitharu.kotatsu.core.ui.image.AnimatedFaviconDrawable
import org.koitharu.kotatsu.core.ui.image.FaviconDrawable
import org.koitharu.kotatsu.core.ui.image.TrimTransformation
import org.koitharu.kotatsu.core.ui.list.AdapterDelegateClickListenerAdapter
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.util.ext.defaultPlaceholders
import org.koitharu.kotatsu.core.util.ext.drawableStart
import org.koitharu.kotatsu.core.util.ext.enqueueWith
import org.koitharu.kotatsu.core.util.ext.mangaSourceExtra
import org.koitharu.kotatsu.core.util.ext.newImageRequest
import org.koitharu.kotatsu.core.util.ext.recyclerView
import org.koitharu.kotatsu.core.util.ext.setProgressIcon
import org.koitharu.kotatsu.core.util.ext.textAndVisible
@ -63,15 +48,13 @@ fun exploreButtonsAD(
}
fun exploreRecommendationItemAD(
coil: ImageLoader,
itemClickListener: OnListItemClickListener<Manga>,
lifecycleOwner: LifecycleOwner,
) = adapterDelegateViewBinding<RecommendationsItem, ListModel, ItemRecommendationBinding>(
{ layoutInflater, parent -> ItemRecommendationBinding.inflate(layoutInflater, parent, false) },
) {
val adapter = BaseListAdapter<MangaCompactListModel>()
.addDelegate(ListItemType.MANGA_LIST, recommendationMangaItemAD(coil, itemClickListener, lifecycleOwner))
.addDelegate(ListItemType.MANGA_LIST, recommendationMangaItemAD(itemClickListener))
binding.pager.adapter = adapter
binding.pager.recyclerView?.isNestedScrollingEnabled = false
binding.dots.bindToViewPager(binding.pager)
@ -82,9 +65,7 @@ fun exploreRecommendationItemAD(
}
fun recommendationMangaItemAD(
coil: ImageLoader,
itemClickListener: OnListItemClickListener<Manga>,
lifecycleOwner: LifecycleOwner,
) = adapterDelegateViewBinding<MangaCompactListModel, MangaCompactListModel, ItemRecommendationMangaBinding>(
{ layoutInflater, parent -> ItemRecommendationMangaBinding.inflate(layoutInflater, parent, false) },
) {
@ -95,21 +76,13 @@ fun recommendationMangaItemAD(
bind {
binding.textViewTitle.text = item.manga.title
binding.textViewSubtitle.textAndVisible = item.subtitle
binding.imageViewCover.newImageRequest(lifecycleOwner, item.manga.coverUrl)?.run {
defaultPlaceholders(context)
allowRgb565(true)
transformations(TrimTransformation())
mangaSourceExtra(item.manga.source)
enqueueWith(coil)
}
binding.imageViewCover.setImageAsync(item.manga.coverUrl, item.manga.source)
}
}
fun exploreSourceListItemAD(
coil: ImageLoader,
listener: OnListItemClickListener<MangaSourceItem>,
lifecycleOwner: LifecycleOwner,
) = adapterDelegateViewBinding<MangaSourceItem, ListModel, ItemExploreSourceListBinding>(
{ layoutInflater, parent ->
ItemExploreSourceListBinding.inflate(
@ -128,21 +101,12 @@ fun exploreSourceListItemAD(
binding.textViewTitle.text = item.source.getTitle(context)
binding.textViewTitle.drawableStart = if (item.source.isPinned) iconPinned else null
binding.textViewSubtitle.text = item.source.getSummary(context)
val fallbackIcon = FaviconDrawable(context, R.style.FaviconDrawable_Small, item.source.name)
binding.imageViewIcon.newImageRequest(lifecycleOwner, item.source.faviconUri())?.run {
fallback(fallbackIcon)
placeholder(AnimatedFaviconDrawable(context, R.style.FaviconDrawable_Small, item.source.name))
error(fallbackIcon)
mangaSourceExtra(item.source)
enqueueWith(coil)
}
binding.imageViewIcon.setImageAsync(item.source)
}
}
fun exploreSourceGridItemAD(
coil: ImageLoader,
listener: OnListItemClickListener<MangaSourceItem>,
lifecycleOwner: LifecycleOwner,
) = adapterDelegateViewBinding<MangaSourceItem, ListModel, ItemExploreSourceGridBinding>(
{ layoutInflater, parent ->
ItemExploreSourceGridBinding.inflate(
@ -160,13 +124,6 @@ fun exploreSourceGridItemAD(
bind {
binding.textViewTitle.text = item.source.getTitle(context)
binding.textViewTitle.drawableStart = if (item.source.isPinned) iconPinned else null
val fallbackIcon = FaviconDrawable(context, R.style.FaviconDrawable_Large, item.source.name)
binding.imageViewIcon.newImageRequest(lifecycleOwner, item.source.faviconUri())?.run {
fallback(fallbackIcon)
placeholder(AnimatedFaviconDrawable(context, R.style.FaviconDrawable_Large, item.source.name))
error(fallbackIcon)
mangaSourceExtra(item.source)
enqueueWith(coil)
}
binding.imageViewIcon.setImageAsync(item.source)
}
}

@ -50,7 +50,7 @@ class FavouriteCategoriesActivity :
super.onCreate(savedInstanceState)
setContentView(ActivityCategoriesBinding.inflate(layoutInflater))
setDisplayHomeAsUp(true, false)
adapter = CategoriesAdapter(coil, this, this, this)
adapter = CategoriesAdapter(this, this)
selectionController = ListSelectionController(
appCompatDelegate = delegate,
decoration = CategoriesSelectionDecoration(this),

@ -1,7 +1,5 @@
package org.koitharu.kotatsu.favourites.ui.categories.adapter
import androidx.lifecycle.LifecycleOwner
import coil3.ImageLoader
import org.koitharu.kotatsu.core.ui.ReorderableListAdapter
import org.koitharu.kotatsu.favourites.ui.categories.FavouriteCategoriesListListener
import org.koitharu.kotatsu.list.ui.adapter.ListItemType
@ -11,16 +9,14 @@ import org.koitharu.kotatsu.list.ui.adapter.loadingStateAD
import org.koitharu.kotatsu.list.ui.model.ListModel
class CategoriesAdapter(
coil: ImageLoader,
lifecycleOwner: LifecycleOwner,
onItemClickListener: FavouriteCategoriesListListener,
listListener: ListStateHolderListener,
) : ReorderableListAdapter<ListModel>() {
init {
addDelegate(ListItemType.CATEGORY_LARGE, categoryAD(coil, lifecycleOwner, onItemClickListener))
addDelegate(ListItemType.NAV_ITEM, allCategoriesAD(coil, lifecycleOwner, onItemClickListener))
addDelegate(ListItemType.STATE_EMPTY, emptyStateListAD(coil, lifecycleOwner, listListener))
addDelegate(ListItemType.CATEGORY_LARGE, categoryAD(onItemClickListener))
addDelegate(ListItemType.NAV_ITEM, allCategoriesAD(onItemClickListener))
addDelegate(ListItemType.STATE_EMPTY, emptyStateListAD(listListener))
addDelegate(ListItemType.STATE_LOADING, loadingStateAD())
}
}

@ -1,33 +1,16 @@
package org.koitharu.kotatsu.favourites.ui.categories.adapter
import android.annotation.SuppressLint
import android.content.res.ColorStateList
import android.graphics.Color
import android.graphics.drawable.ColorDrawable
import android.view.MotionEvent
import android.view.View
import android.view.View.OnClickListener
import android.view.View.OnLongClickListener
import android.view.View.OnTouchListener
import androidx.core.graphics.ColorUtils
import androidx.core.view.isGone
import androidx.core.view.isVisible
import androidx.core.widget.ImageViewCompat
import androidx.lifecycle.LifecycleOwner
import coil3.ImageLoader
import coil3.request.allowRgb565
import coil3.request.crossfade
import coil3.request.error
import coil3.request.fallback
import coil3.request.placeholder
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.util.ext.enqueueWith
import org.koitharu.kotatsu.core.util.ext.getAnimationDuration
import org.koitharu.kotatsu.core.util.ext.getQuantityStringSafe
import org.koitharu.kotatsu.core.util.ext.getThemeColor
import org.koitharu.kotatsu.core.util.ext.mangaSourceExtra
import org.koitharu.kotatsu.core.util.ext.newImageRequest
import org.koitharu.kotatsu.databinding.ItemCategoriesAllBinding
import org.koitharu.kotatsu.databinding.ItemCategoryBinding
import org.koitharu.kotatsu.favourites.ui.categories.FavouriteCategoriesListListener
@ -35,8 +18,6 @@ import org.koitharu.kotatsu.list.ui.model.ListModel
@SuppressLint("ClickableViewAccessibility")
fun categoryAD(
coil: ImageLoader,
lifecycleOwner: LifecycleOwner,
clickListener: FavouriteCategoriesListListener,
) = adapterDelegateViewBinding<CategoryListModel, ListModel, ItemCategoryBinding>(
{ inflater, parent -> ItemCategoryBinding.inflate(inflater, parent, false) },
@ -52,22 +33,6 @@ fun categoryAD(
override fun onTouch(v: View?, event: MotionEvent): Boolean = event.actionMasked == MotionEvent.ACTION_DOWN &&
clickListener.onDragHandleTouch(this@adapterDelegateViewBinding)
}
val backgroundColor = context.getThemeColor(android.R.attr.colorBackground)
ImageViewCompat.setImageTintList(
binding.imageViewCover3,
ColorStateList.valueOf(ColorUtils.setAlphaComponent(backgroundColor, 153)),
)
ImageViewCompat.setImageTintList(
binding.imageViewCover2,
ColorStateList.valueOf(ColorUtils.setAlphaComponent(backgroundColor, 76)),
)
binding.imageViewCover2.backgroundTintList =
ColorStateList.valueOf(ColorUtils.setAlphaComponent(backgroundColor, 76))
binding.imageViewCover3.backgroundTintList =
ColorStateList.valueOf(ColorUtils.setAlphaComponent(backgroundColor, 153))
val fallback = ColorDrawable(Color.TRANSPARENT)
val coverViews = arrayOf(binding.imageViewCover1, binding.imageViewCover2, binding.imageViewCover3)
val crossFadeDuration = context.getAnimationDuration(R.integer.config_defaultAnimTime).toInt()
itemView.setOnClickListener(eventListener)
itemView.setOnLongClickListener(eventListener)
binding.imageViewEdit.setOnClickListener(eventListener)
@ -88,24 +53,11 @@ fun categoryAD(
}
binding.imageViewTracker.isVisible = item.category.isTrackingEnabled
binding.imageViewHidden.isGone = item.category.isVisibleInLibrary
repeat(coverViews.size) { i ->
val cover = item.covers.getOrNull(i)
coverViews[i].newImageRequest(lifecycleOwner, cover?.url)?.run {
placeholder(R.drawable.ic_placeholder)
fallback(fallback)
mangaSourceExtra(cover?.mangaSource)
crossfade(crossFadeDuration * (i + 1))
error(R.drawable.ic_error_placeholder)
allowRgb565(true)
enqueueWith(coil)
}
}
binding.coversView.setCoversAsync(item.covers)
}
}
fun allCategoriesAD(
coil: ImageLoader,
lifecycleOwner: LifecycleOwner,
clickListener: FavouriteCategoriesListListener,
) = adapterDelegateViewBinding<AllCategoriesListModel, ListModel, ItemCategoriesAllBinding>(
{ inflater, parent -> ItemCategoriesAllBinding.inflate(inflater, parent, false) },
@ -117,22 +69,7 @@ fun allCategoriesAD(
clickListener.onItemClick(null, v)
}
}
val backgroundColor = context.getThemeColor(android.R.attr.colorBackground)
ImageViewCompat.setImageTintList(
binding.imageViewCover3,
ColorStateList.valueOf(ColorUtils.setAlphaComponent(backgroundColor, 153)),
)
ImageViewCompat.setImageTintList(
binding.imageViewCover2,
ColorStateList.valueOf(ColorUtils.setAlphaComponent(backgroundColor, 76)),
)
binding.imageViewCover2.backgroundTintList =
ColorStateList.valueOf(ColorUtils.setAlphaComponent(backgroundColor, 76))
binding.imageViewCover3.backgroundTintList =
ColorStateList.valueOf(ColorUtils.setAlphaComponent(backgroundColor, 153))
val fallback = ColorDrawable(Color.TRANSPARENT)
val coverViews = arrayOf(binding.imageViewCover1, binding.imageViewCover2, binding.imageViewCover3)
val crossFadeDuration = context.getAnimationDuration(R.integer.config_defaultAnimTime).toInt()
itemView.setOnClickListener(eventListener)
binding.imageViewVisible.setOnClickListener(eventListener)
@ -154,17 +91,6 @@ fun allCategoriesAD(
R.drawable.ic_eye_off
},
)
repeat(coverViews.size) { i ->
val cover = item.covers.getOrNull(i)
coverViews[i].newImageRequest(lifecycleOwner, cover?.url)?.run {
placeholder(R.drawable.ic_placeholder)
fallback(fallback)
mangaSourceExtra(cover?.mangaSource)
crossfade(crossFadeDuration * (i + 1))
error(R.drawable.ic_error_placeholder)
allowRgb565(true)
enqueueWith(coil)
}
}
binding.coversView.setCoversAsync(item.covers)
}
}

@ -1,24 +1,12 @@
package org.koitharu.kotatsu.favourites.ui.categories.select
import android.content.DialogInterface
import android.content.res.ColorStateList
import android.graphics.Color
import android.graphics.drawable.ColorDrawable
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import androidx.core.graphics.ColorUtils
import androidx.core.view.isVisible
import androidx.core.widget.ImageViewCompat
import androidx.fragment.app.viewModels
import coil3.ImageLoader
import coil3.request.allowRgb565
import coil3.request.crossfade
import coil3.request.error
import coil3.request.fallback
import coil3.request.placeholder
import com.google.android.material.checkbox.MaterialCheckBox
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import dagger.hilt.android.AndroidEntryPoint
@ -26,20 +14,13 @@ import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.nav.router
import org.koitharu.kotatsu.core.ui.AlertDialogFragment
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.util.ext.disposeImageRequest
import org.koitharu.kotatsu.core.util.ext.enqueueWith
import org.koitharu.kotatsu.core.util.ext.getAnimationDuration
import org.koitharu.kotatsu.core.util.ext.getDisplayMessage
import org.koitharu.kotatsu.core.util.ext.getThemeColor
import org.koitharu.kotatsu.core.util.ext.joinToStringWithLimit
import org.koitharu.kotatsu.core.util.ext.mangaSourceExtra
import org.koitharu.kotatsu.core.util.ext.newImageRequest
import org.koitharu.kotatsu.core.util.ext.observe
import org.koitharu.kotatsu.core.util.ext.observeEvent
import org.koitharu.kotatsu.databinding.SheetFavoriteCategoriesBinding
import org.koitharu.kotatsu.favourites.ui.categories.select.adapter.MangaCategoriesAdapter
import org.koitharu.kotatsu.favourites.ui.categories.select.model.MangaCategoryItem
import javax.inject.Inject
@AndroidEntryPoint
class FavoriteDialog : AlertDialogFragment<SheetFavoriteCategoriesBinding>(),
@ -47,9 +28,6 @@ class FavoriteDialog : AlertDialogFragment<SheetFavoriteCategoriesBinding>(),
private val viewModel by viewModels<FavoriteDialogViewModel>()
@Inject
lateinit var coil: ImageLoader
override fun onCreateViewBinding(
inflater: LayoutInflater,
container: ViewGroup?,
@ -66,7 +44,7 @@ class FavoriteDialog : AlertDialogFragment<SheetFavoriteCategoriesBinding>(),
savedInstanceState: Bundle?,
) {
super.onViewBindingCreated(binding, savedInstanceState)
val adapter = MangaCategoriesAdapter(coil, viewLifecycleOwner, this)
val adapter = MangaCategoriesAdapter(this)
binding.recyclerViewCategories.adapter = adapter
viewModel.content.observe(viewLifecycleOwner, adapter)
viewModel.onError.observeEvent(viewLifecycleOwner, ::onError)
@ -88,42 +66,7 @@ class FavoriteDialog : AlertDialogFragment<SheetFavoriteCategoriesBinding>(),
private fun bindHeader() {
val manga = viewModel.manga
val binding = viewBinding ?: return
val backgroundColor = binding.root.context.getThemeColor(android.R.attr.colorBackground)
ImageViewCompat.setImageTintList(
binding.imageViewCover3,
ColorStateList.valueOf(ColorUtils.setAlphaComponent(backgroundColor, 153)),
)
ImageViewCompat.setImageTintList(
binding.imageViewCover2,
ColorStateList.valueOf(ColorUtils.setAlphaComponent(backgroundColor, 76)),
)
binding.imageViewCover2.backgroundTintList =
ColorStateList.valueOf(ColorUtils.setAlphaComponent(backgroundColor, 76))
binding.imageViewCover3.backgroundTintList =
ColorStateList.valueOf(ColorUtils.setAlphaComponent(backgroundColor, 153))
val fallback = ColorDrawable(Color.TRANSPARENT)
val coverViews = arrayOf(binding.imageViewCover1, binding.imageViewCover2, binding.imageViewCover3)
val crossFadeDuration = binding.root.context.getAnimationDuration(R.integer.config_defaultAnimTime).toInt()
binding.textViewTitle.text = manga.joinToStringWithLimit(binding.root.context, 92) { it.title }
repeat(coverViews.size) { i ->
val m = manga.getOrNull(i)
val view = coverViews[i]
view.isVisible = m != null
if (m == null) {
view.disposeImageRequest()
} else {
view.newImageRequest(viewLifecycleOwner, m.coverUrl)?.run {
placeholder(R.drawable.ic_placeholder)
fallback(fallback)
mangaSourceExtra(m.source)
crossfade(crossFadeDuration * (i + 1))
error(R.drawable.ic_error_placeholder)
allowRgb565(true)
enqueueWith(coil)
}
}
}
binding.coversStack.setCoversAsync(manga)
}
}

@ -1,7 +1,5 @@
package org.koitharu.kotatsu.favourites.ui.categories.select.adapter
import androidx.lifecycle.LifecycleOwner
import coil3.ImageLoader
import org.koitharu.kotatsu.core.ui.BaseListAdapter
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.favourites.ui.categories.select.model.MangaCategoryItem
@ -11,14 +9,12 @@ import org.koitharu.kotatsu.list.ui.adapter.loadingStateAD
import org.koitharu.kotatsu.list.ui.model.ListModel
class MangaCategoriesAdapter(
coil: ImageLoader,
lifecycleOwner: LifecycleOwner,
clickListener: OnListItemClickListener<MangaCategoryItem>,
) : BaseListAdapter<ListModel>() {
init {
addDelegate(ListItemType.NAV_ITEM, mangaCategoryAD(clickListener))
addDelegate(ListItemType.STATE_LOADING, loadingStateAD())
addDelegate(ListItemType.STATE_EMPTY, emptyStateListAD(coil, lifecycleOwner, null))
addDelegate(ListItemType.STATE_EMPTY, emptyStateListAD(null))
}
}

@ -12,7 +12,6 @@ import androidx.core.view.isVisible
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.recyclerview.widget.RecyclerView
import coil3.ImageLoader
import com.google.android.material.tabs.TabLayoutMediator
import dagger.hilt.android.AndroidEntryPoint
import org.koitharu.kotatsu.R
@ -22,9 +21,7 @@ import org.koitharu.kotatsu.core.ui.util.ActionModeListener
import org.koitharu.kotatsu.core.ui.util.RecyclerViewOwner
import org.koitharu.kotatsu.core.ui.util.ReversibleActionObserver
import org.koitharu.kotatsu.core.util.ext.addMenuProvider
import org.koitharu.kotatsu.core.util.ext.enqueueWith
import org.koitharu.kotatsu.core.util.ext.findCurrentPagerFragment
import org.koitharu.kotatsu.core.util.ext.newImageRequest
import org.koitharu.kotatsu.core.util.ext.observe
import org.koitharu.kotatsu.core.util.ext.observeEvent
import org.koitharu.kotatsu.core.util.ext.recyclerView
@ -32,7 +29,6 @@ import org.koitharu.kotatsu.core.util.ext.setTabsEnabled
import org.koitharu.kotatsu.core.util.ext.setTextAndVisible
import org.koitharu.kotatsu.databinding.FragmentFavouritesContainerBinding
import org.koitharu.kotatsu.databinding.ItemEmptyStateBinding
import javax.inject.Inject
@AndroidEntryPoint
class FavouritesContainerFragment : BaseFragment<FragmentFavouritesContainerBinding>(),
@ -41,9 +37,6 @@ class FavouritesContainerFragment : BaseFragment<FragmentFavouritesContainerBind
ViewStub.OnInflateListener,
View.OnClickListener {
@Inject
lateinit var coil: ImageLoader
private val viewModel: FavouritesContainerViewModel by viewModels()
override val recyclerView: RecyclerView?
@ -96,7 +89,7 @@ class FavouritesContainerFragment : BaseFragment<FragmentFavouritesContainerBind
override fun onInflate(stub: ViewStub?, inflated: View) {
val stubBinding = ItemEmptyStateBinding.bind(inflated)
stubBinding.icon.newImageRequest(viewLifecycleOwner, R.drawable.ic_empty_favourites)?.enqueueWith(coil)
stubBinding.icon.setImageAsync(R.drawable.ic_empty_favourites)
stubBinding.textPrimary.setText(R.string.text_empty_holder_primary)
stubBinding.textSecondary.setTextAndVisible(R.string.empty_favourite_categories)
stubBinding.buttonRetry.setTextAndVisible(R.string.manage)

@ -1,19 +1,15 @@
package org.koitharu.kotatsu.history.ui
import android.content.Context
import androidx.lifecycle.LifecycleOwner
import coil3.ImageLoader
import org.koitharu.kotatsu.core.ui.list.fastscroll.FastScroller
import org.koitharu.kotatsu.list.ui.adapter.MangaListAdapter
import org.koitharu.kotatsu.list.ui.adapter.MangaListListener
import org.koitharu.kotatsu.list.ui.size.ItemSizeResolver
class HistoryListAdapter(
coil: ImageLoader,
lifecycleOwner: LifecycleOwner,
listener: MangaListListener,
sizeResolver: ItemSizeResolver,
) : MangaListAdapter(coil, lifecycleOwner, listener, sizeResolver), FastScroller.SectionIndexer {
) : MangaListAdapter(listener, sizeResolver), FastScroller.SectionIndexer {
override fun getSectionText(context: Context, position: Int): CharSequence? {
return findHeader(position)?.getText(context)

@ -73,9 +73,7 @@ class HistoryListFragment : MangaListFragment() {
}
override fun onCreateAdapter() = HistoryListAdapter(
coil,
viewLifecycleOwner,
this,
DynamicItemSizeResolver(resources, settings, adjustWidth = false),
DynamicItemSizeResolver(resources, viewLifecycleOwner, settings, adjustWidth = false),
)
}

@ -0,0 +1,243 @@
package org.koitharu.kotatsu.image.ui
import android.content.Context
import android.util.AttributeSet
import android.view.ViewGroup
import android.view.ViewTreeObserver
import android.view.ViewTreeObserver.OnPreDrawListener
import androidx.annotation.AttrRes
import androidx.core.content.withStyledAttributes
import androidx.core.graphics.ColorUtils
import androidx.core.graphics.drawable.toDrawable
import coil3.request.transformations
import coil3.size.Dimension
import coil3.size.Size
import coil3.size.ViewSizeResolver
import kotlinx.coroutines.suspendCancellableCoroutine
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.bookmarks.domain.Bookmark
import org.koitharu.kotatsu.core.image.CoilImageView
import org.koitharu.kotatsu.core.ui.image.AnimatedPlaceholderDrawable
import org.koitharu.kotatsu.core.ui.image.TrimTransformation
import org.koitharu.kotatsu.core.util.ext.bookmarkExtra
import org.koitharu.kotatsu.core.util.ext.decodeRegion
import org.koitharu.kotatsu.core.util.ext.getThemeColor
import org.koitharu.kotatsu.core.util.ext.mangaExtra
import org.koitharu.kotatsu.core.util.ext.mangaSourceExtra
import org.koitharu.kotatsu.favourites.domain.model.Cover
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaPage
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.util.nullIfEmpty
import org.koitharu.kotatsu.reader.ui.pager.ReaderPage
import kotlin.coroutines.resume
import androidx.appcompat.R as appcompatR
import com.google.android.material.R as materialR
class CoverImageView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
@AttrRes defStyleAttr: Int = R.attr.coverImageViewStyle,
) : CoilImageView(context, attrs, defStyleAttr) {
private var aspectRationHeight: Int = 0
private var aspectRationWidth: Int = 0
var trimImage: Boolean = false
private val hasAspectRatio: Boolean
get() = aspectRationHeight > 0 && aspectRationWidth > 0
init {
context.withStyledAttributes(attrs, R.styleable.CoverImageView, defStyleAttr) {
aspectRationHeight = getInt(R.styleable.CoverImageView_aspectRationHeight, aspectRationHeight)
aspectRationWidth = getInt(R.styleable.CoverImageView_aspectRationWidth, aspectRationWidth)
trimImage = getBoolean(R.styleable.CoverImageView_trimImage, trimImage)
}
if (placeholderDrawable == null) {
placeholderDrawable = AnimatedPlaceholderDrawable(context)
}
if (errorDrawable == null) {
errorDrawable = ColorUtils.blendARGB(
context.getThemeColor(materialR.attr.colorErrorContainer),
context.getThemeColor(appcompatR.attr.colorBackgroundFloating),
0.25f,
).toDrawable()
}
if (fallbackDrawable == null) {
fallbackDrawable = context.getThemeColor(materialR.attr.colorSurfaceContainer).toDrawable()
}
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
if (!hasAspectRatio) {
return
}
val isExactWidth = MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.EXACTLY
val isExactHeight = MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.EXACTLY
when {
isExactHeight && isExactWidth -> Unit
isExactHeight -> setMeasuredDimension(
/* measuredWidth = */ measuredHeight * aspectRationWidth / aspectRationHeight,
/* measuredHeight = */ measuredHeight,
)
isExactWidth -> setMeasuredDimension(
/* measuredWidth = */ measuredWidth,
/* measuredHeight = */ measuredWidth * aspectRationHeight / aspectRationWidth,
)
}
}
fun setImageAsync(page: ReaderPage) = enqueueRequest(
newRequestBuilder()
.data(page.preview?.nullIfEmpty() ?: page.toMangaPage())
.mangaSourceExtra(page.source)
.build(),
)
fun setImageAsync(page: MangaPage) = enqueueRequest(
newRequestBuilder()
.data(page.preview?.nullIfEmpty() ?: page)
.mangaSourceExtra(page.source)
.build(),
)
fun setImageAsync(cover: Cover?) = enqueueRequest(
newRequestBuilder()
.data(cover?.url)
.mangaSourceExtra(cover?.mangaSource)
.build(),
)
fun setImageAsync(
coverUrl: String?,
manga: Manga?,
) = enqueueRequest(
newRequestBuilder()
.data(coverUrl)
.mangaExtra(manga)
.build(),
)
fun setImageAsync(
coverUrl: String?,
source: MangaSource,
) = enqueueRequest(
newRequestBuilder()
.data(coverUrl)
.mangaSourceExtra(source)
.build(),
)
fun setImageAsync(
bookmark: Bookmark
) = enqueueRequest(
newRequestBuilder()
.data(bookmark.imageLoadData)
.decodeRegion(bookmark.scroll)
.bookmarkExtra(bookmark)
.build(),
)
override fun newRequestBuilder() = super.newRequestBuilder().apply {
if (trimImage) {
transformations(listOf(TrimTransformation()))
}
if (hasAspectRatio) {
size(CoverSizeResolver(this@CoverImageView))
}
}
private class CoverSizeResolver(
override val view: CoverImageView,
) : ViewSizeResolver<CoverImageView> {
override suspend fun size(): Size {
// Fast path: the view is already measured.
getSize()?.let { return it }
// Slow path: wait for the view to be measured.
return suspendCancellableCoroutine { continuation ->
val viewTreeObserver = view.viewTreeObserver
val preDrawListener = object : OnPreDrawListener {
private var isResumed = false
override fun onPreDraw(): Boolean {
val size = getSize()
if (size != null) {
viewTreeObserver.removePreDrawListenerSafe(this)
if (!isResumed) {
isResumed = true
continuation.resume(size)
}
}
return true
}
}
viewTreeObserver.addOnPreDrawListener(preDrawListener)
continuation.invokeOnCancellation {
viewTreeObserver.removePreDrawListenerSafe(preDrawListener)
}
}
}
private fun getSize(): Size? {
var width = getWidth()
var height = getHeight()
when {
width == null && height == null -> {
return null
}
height == null -> {
height = Dimension(width!!.px * view.aspectRationHeight / view.aspectRationWidth)
}
width == null -> {
width = Dimension(height.px * view.aspectRationWidth / view.aspectRationHeight)
}
}
return Size(checkNotNull(width), checkNotNull(height))
}
private fun getWidth() = getDimension(
paramSize = view.layoutParams?.width ?: -1,
viewSize = view.width,
paddingSize = if (subtractPadding) view.paddingLeft + view.paddingRight else 0,
)
private fun getHeight() = getDimension(
paramSize = view.layoutParams?.height ?: -1,
viewSize = view.height,
paddingSize = if (subtractPadding) view.paddingTop + view.paddingBottom else 0,
)
private fun getDimension(paramSize: Int, viewSize: Int, paddingSize: Int): Dimension.Pixels? {
if (paramSize == ViewGroup.LayoutParams.WRAP_CONTENT) {
return null
}
val insetParamSize = paramSize - paddingSize
if (insetParamSize > 0) {
return Dimension(insetParamSize)
}
val insetViewSize = viewSize - paddingSize
if (insetViewSize > 0) {
return Dimension(insetViewSize)
}
return null
}
private fun ViewTreeObserver.removePreDrawListenerSafe(victim: OnPreDrawListener) {
if (isAlive) {
removeOnPreDrawListener(victim)
} else {
view.viewTreeObserver.removeOnPreDrawListener(victim)
}
}
}
}

@ -0,0 +1,103 @@
package org.koitharu.kotatsu.image.ui
import android.content.Context
import android.content.res.ColorStateList
import android.util.AttributeSet
import android.view.LayoutInflater
import androidx.annotation.AttrRes
import androidx.annotation.Px
import androidx.core.content.withStyledAttributes
import androidx.core.graphics.ColorUtils
import androidx.core.view.children
import androidx.core.view.isVisible
import androidx.core.view.updateLayoutParams
import androidx.core.widget.ImageViewCompat
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.model.UnknownMangaSource
import org.koitharu.kotatsu.core.ui.widgets.StackLayout
import org.koitharu.kotatsu.core.util.ext.getThemeColor
import org.koitharu.kotatsu.databinding.ViewCoverStackBinding
import org.koitharu.kotatsu.favourites.domain.model.Cover
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaSource
class CoverStackView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
@AttrRes defStyleAttr: Int = 0,
) : StackLayout(context, attrs, defStyleAttr) {
private val binding = ViewCoverStackBinding.inflate(LayoutInflater.from(context), this)
private val coverViews = arrayOf(
binding.imageViewCover1,
binding.imageViewCover2,
binding.imageViewCover3,
)
private var hideEmptyView: Boolean = true
init {
context.withStyledAttributes(attrs, R.styleable.CoverStackView, defStyleAttr) {
val coverSize = getDimension(R.styleable.CoverStackView_coverSize, 0f)
if (coverSize > 0f) {
setCoverSize(coverSize)
}
}
val backgroundColor = context.getThemeColor(android.R.attr.colorBackground)
ImageViewCompat.setImageTintList(
binding.imageViewCover3,
ColorStateList.valueOf(ColorUtils.setAlphaComponent(backgroundColor, 153)),
)
ImageViewCompat.setImageTintList(
binding.imageViewCover2,
ColorStateList.valueOf(ColorUtils.setAlphaComponent(backgroundColor, 76)),
)
binding.imageViewCover2.backgroundTintList = ColorStateList.valueOf(
ColorUtils.setAlphaComponent(backgroundColor, 76),
)
binding.imageViewCover3.backgroundTintList = ColorStateList.valueOf(
ColorUtils.setAlphaComponent(backgroundColor, 153),
)
coverViews.forEachIndexed { index, view ->
view.crossfadeDurationFactor = index + 1f
}
}
fun setCoversAsync(covers: List<Cover>) {
coverViews.forEachIndexed { index, view ->
view.setImageAsync(covers.getOrNull(index))
}
}
@JvmName("setMangaCoversAsync")
fun setCoversAsync(manga: List<Manga>) {
coverViews.forEachIndexed { index, view ->
val m = manga.getOrNull(index)
view.setCoverOrHide(m?.coverUrl, m, m?.source)
}
}
fun setCoverSize(@Px coverSize: Float) {
val coverWidth = (coverSize * 13f).toInt()
val coverHeight = (coverSize * 18f).toInt()
children.forEach {
it.updateLayoutParams {
width = coverWidth
height = coverHeight
}
}
}
private fun CoverImageView.setCoverOrHide(url: String?, manga: Manga?, source: MangaSource?) {
if (url.isNullOrEmpty() && hideEmptyView) {
disposeImage()
isVisible = false
} else {
isVisible = true
if (manga != null) {
setImageAsync(url, manga)
} else {
setImageAsync(url, source ?: UnknownMangaSource)
}
}
}
}

@ -218,10 +218,8 @@ abstract class MangaListFragment :
protected open fun onCreateAdapter(): MangaListAdapter {
return MangaListAdapter(
coil = coil,
lifecycleOwner = viewLifecycleOwner,
listener = this,
sizeResolver = DynamicItemSizeResolver(resources, settings, adjustWidth = false),
sizeResolver = DynamicItemSizeResolver(resources, viewLifecycleOwner, settings, adjustWidth = false),
)
}

@ -1,18 +1,12 @@
package org.koitharu.kotatsu.list.ui.adapter
import androidx.lifecycle.LifecycleOwner
import coil3.ImageLoader
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
import org.koitharu.kotatsu.core.util.ext.enqueueWith
import org.koitharu.kotatsu.core.util.ext.newImageRequest
import org.koitharu.kotatsu.core.util.ext.setTextAndVisible
import org.koitharu.kotatsu.databinding.ItemEmptyCardBinding
import org.koitharu.kotatsu.list.ui.model.EmptyHint
import org.koitharu.kotatsu.list.ui.model.ListModel
fun emptyHintAD(
coil: ImageLoader,
lifecycleOwner: LifecycleOwner,
listener: ListStateHolderListener,
) = adapterDelegateViewBinding<EmptyHint, ListModel, ItemEmptyCardBinding>(
{ inflater, parent -> ItemEmptyCardBinding.inflate(inflater, parent, false) },
@ -21,7 +15,7 @@ fun emptyHintAD(
binding.buttonRetry.setOnClickListener { listener.onEmptyActionClick() }
bind {
binding.icon.newImageRequest(lifecycleOwner, item.icon)?.enqueueWith(coil)
binding.icon.setImageAsync(item.icon)
binding.textPrimary.setText(item.textPrimary)
binding.textSecondary.setTextAndVisible(item.textSecondary)
binding.buttonRetry.setTextAndVisible(item.actionStringRes)

@ -1,20 +1,13 @@
package org.koitharu.kotatsu.list.ui.adapter
import androidx.core.view.isVisible
import androidx.lifecycle.LifecycleOwner
import coil3.ImageLoader
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
import org.koitharu.kotatsu.core.util.ext.disposeImageRequest
import org.koitharu.kotatsu.core.util.ext.enqueueWith
import org.koitharu.kotatsu.core.util.ext.newImageRequest
import org.koitharu.kotatsu.core.util.ext.setTextAndVisible
import org.koitharu.kotatsu.databinding.ItemEmptyStateBinding
import org.koitharu.kotatsu.list.ui.model.EmptyState
import org.koitharu.kotatsu.list.ui.model.ListModel
fun emptyStateListAD(
coil: ImageLoader,
lifecycleOwner: LifecycleOwner,
listener: ListStateHolderListener?,
) = adapterDelegateViewBinding<EmptyState, ListModel, ItemEmptyStateBinding>(
{ inflater, parent -> ItemEmptyStateBinding.inflate(inflater, parent, false) },
@ -27,10 +20,10 @@ fun emptyStateListAD(
bind {
if (item.icon == 0) {
binding.icon.isVisible = false
binding.icon.disposeImageRequest()
binding.icon.disposeImage()
} else {
binding.icon.isVisible = true
binding.icon.newImageRequest(lifecycleOwner, item.icon)?.enqueueWith(coil)
binding.icon.setImageAsync(item.icon)
}
binding.textPrimary.setText(item.textPrimary)
binding.textSecondary.setTextAndVisible(item.textSecondary)

@ -1,20 +1,10 @@
package org.koitharu.kotatsu.list.ui.adapter
import androidx.core.view.isVisible
import androidx.lifecycle.LifecycleOwner
import coil3.ImageLoader
import coil3.request.allowRgb565
import coil3.request.transformations
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.ui.image.CoverSizeResolver
import org.koitharu.kotatsu.core.ui.image.TrimTransformation
import org.koitharu.kotatsu.core.ui.list.AdapterDelegateClickListenerAdapter
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.util.ext.defaultPlaceholders
import org.koitharu.kotatsu.core.util.ext.enqueueWith
import org.koitharu.kotatsu.core.util.ext.mangaExtra
import org.koitharu.kotatsu.core.util.ext.newImageRequest
import org.koitharu.kotatsu.databinding.ItemMangaGridBinding
import org.koitharu.kotatsu.list.ui.ListModelDiffCallback.Companion.PAYLOAD_PROGRESS_CHANGED
import org.koitharu.kotatsu.list.ui.model.ListModel
@ -23,8 +13,6 @@ import org.koitharu.kotatsu.list.ui.size.ItemSizeResolver
import org.koitharu.kotatsu.parsers.model.Manga
fun mangaGridItemAD(
coil: ImageLoader,
lifecycleOwner: LifecycleOwner,
sizeResolver: ItemSizeResolver,
clickListener: OnListItemClickListener<Manga>,
) = adapterDelegateViewBinding<MangaGridModel, ListModel, ItemMangaGridBinding>(
@ -32,7 +20,7 @@ fun mangaGridItemAD(
) {
AdapterDelegateClickListenerAdapter(this, clickListener, MangaGridModel::manga).attach(itemView)
sizeResolver.attachToView(lifecycleOwner, itemView, binding.textViewTitle, binding.progressView)
sizeResolver.attachToView(itemView, binding.textViewTitle, binding.progressView)
bind { payloads ->
binding.textViewTitle.text = item.title
@ -43,14 +31,7 @@ fun mangaGridItemAD(
if (item.isFavorite) addIcon(R.drawable.ic_heart_outline)
isVisible = iconsCount > 0
}
binding.imageViewCover.newImageRequest(lifecycleOwner, item.coverUrl)?.run {
size(CoverSizeResolver(binding.imageViewCover))
defaultPlaceholders(context)
transformations(TrimTransformation())
allowRgb565(true)
mangaExtra(item.manga)
enqueueWith(coil)
}
binding.imageViewCover.setImageAsync(item.coverUrl, item.manga)
binding.badge.number = item.counter
binding.badge.isVisible = item.counter > 0
}

@ -1,28 +1,24 @@
package org.koitharu.kotatsu.list.ui.adapter
import androidx.lifecycle.LifecycleOwner
import coil3.ImageLoader
import org.koitharu.kotatsu.core.ui.BaseListAdapter
import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.list.ui.size.ItemSizeResolver
open class MangaListAdapter(
coil: ImageLoader,
lifecycleOwner: LifecycleOwner,
listener: MangaListListener,
sizeResolver: ItemSizeResolver,
) : BaseListAdapter<ListModel>() {
init {
addDelegate(ListItemType.MANGA_LIST, mangaListItemAD(coil, lifecycleOwner, listener))
addDelegate(ListItemType.MANGA_LIST_DETAILED, mangaListDetailedItemAD(coil, lifecycleOwner, listener))
addDelegate(ListItemType.MANGA_GRID, mangaGridItemAD(coil, lifecycleOwner, sizeResolver, listener))
addDelegate(ListItemType.MANGA_LIST, mangaListItemAD(listener))
addDelegate(ListItemType.MANGA_LIST_DETAILED, mangaListDetailedItemAD(listener))
addDelegate(ListItemType.MANGA_GRID, mangaGridItemAD(sizeResolver, listener))
addDelegate(ListItemType.FOOTER_LOADING, loadingFooterAD())
addDelegate(ListItemType.STATE_LOADING, loadingStateAD())
addDelegate(ListItemType.STATE_ERROR, errorStateListAD(listener))
addDelegate(ListItemType.FOOTER_ERROR, errorFooterAD(listener))
addDelegate(ListItemType.STATE_EMPTY, emptyStateListAD(coil, lifecycleOwner, listener))
addDelegate(ListItemType.HINT_EMPTY, emptyHintAD(coil, lifecycleOwner, listener))
addDelegate(ListItemType.STATE_EMPTY, emptyStateListAD(listener))
addDelegate(ListItemType.HINT_EMPTY, emptyHintAD(listener))
addDelegate(ListItemType.HEADER, listHeaderAD(listener))
addDelegate(ListItemType.QUICK_FILTER, quickFilterAD(listener))
addDelegate(ListItemType.TIP, tipAD(listener))

@ -1,19 +1,9 @@
package org.koitharu.kotatsu.list.ui.adapter
import androidx.core.view.isVisible
import androidx.lifecycle.LifecycleOwner
import coil3.ImageLoader
import coil3.request.allowRgb565
import coil3.request.transformations
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.ui.image.CoverSizeResolver
import org.koitharu.kotatsu.core.ui.image.TrimTransformation
import org.koitharu.kotatsu.core.ui.list.AdapterDelegateClickListenerAdapter
import org.koitharu.kotatsu.core.util.ext.defaultPlaceholders
import org.koitharu.kotatsu.core.util.ext.enqueueWith
import org.koitharu.kotatsu.core.util.ext.mangaExtra
import org.koitharu.kotatsu.core.util.ext.newImageRequest
import org.koitharu.kotatsu.core.util.ext.textAndVisible
import org.koitharu.kotatsu.databinding.ItemMangaListDetailsBinding
import org.koitharu.kotatsu.list.ui.ListModelDiffCallback
@ -21,8 +11,6 @@ import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.list.ui.model.MangaDetailedListModel
fun mangaListDetailedItemAD(
coil: ImageLoader,
lifecycleOwner: LifecycleOwner,
clickListener: MangaDetailsClickListener,
) = adapterDelegateViewBinding<MangaDetailedListModel, ListModel, ItemMangaListDetailsBinding>(
{ inflater, parent -> ItemMangaListDetailsBinding.inflate(inflater, parent, false) },
@ -43,14 +31,7 @@ fun mangaListDetailedItemAD(
if (item.isFavorite) addIcon(R.drawable.ic_heart_outline)
isVisible = iconsCount > 0
}
binding.imageViewCover.newImageRequest(lifecycleOwner, item.coverUrl)?.run {
size(CoverSizeResolver(binding.imageViewCover))
defaultPlaceholders(context)
transformations(TrimTransformation())
allowRgb565(true)
mangaExtra(item.manga)
enqueueWith(coil)
}
binding.imageViewCover.setImageAsync(item.coverUrl, item.manga)
binding.textViewTags.text = item.tags.joinToString(separator = ", ") { it.title ?: "" }
binding.badge.number = item.counter
binding.badge.isVisible = item.counter > 0

@ -1,18 +1,9 @@
package org.koitharu.kotatsu.list.ui.adapter
import androidx.core.view.isVisible
import androidx.lifecycle.LifecycleOwner
import coil3.ImageLoader
import coil3.request.allowRgb565
import coil3.request.transformations
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
import org.koitharu.kotatsu.core.ui.image.TrimTransformation
import org.koitharu.kotatsu.core.ui.list.AdapterDelegateClickListenerAdapter
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.util.ext.defaultPlaceholders
import org.koitharu.kotatsu.core.util.ext.enqueueWith
import org.koitharu.kotatsu.core.util.ext.mangaExtra
import org.koitharu.kotatsu.core.util.ext.newImageRequest
import org.koitharu.kotatsu.core.util.ext.textAndVisible
import org.koitharu.kotatsu.databinding.ItemMangaListBinding
import org.koitharu.kotatsu.list.ui.model.ListModel
@ -20,8 +11,6 @@ import org.koitharu.kotatsu.list.ui.model.MangaCompactListModel
import org.koitharu.kotatsu.parsers.model.Manga
fun mangaListItemAD(
coil: ImageLoader,
lifecycleOwner: LifecycleOwner,
clickListener: OnListItemClickListener<Manga>,
) = adapterDelegateViewBinding<MangaCompactListModel, ListModel, ItemMangaListBinding>(
{ inflater, parent -> ItemMangaListBinding.inflate(inflater, parent, false) },
@ -32,13 +21,7 @@ fun mangaListItemAD(
bind {
binding.textViewTitle.text = item.title
binding.textViewSubtitle.textAndVisible = item.subtitle
binding.imageViewCover.newImageRequest(lifecycleOwner, item.coverUrl)?.run {
defaultPlaceholders(context)
allowRgb565(true)
transformations(TrimTransformation())
mangaExtra(item.manga)
enqueueWith(coil)
}
binding.imageViewCover.setImageAsync(item.coverUrl, item.manga)
binding.badge.number = item.counter
binding.badge.isVisible = item.counter > 0
}

@ -10,26 +10,12 @@ import androidx.core.view.WindowInsetsCompat
import androidx.core.view.isVisible
import androidx.fragment.app.viewModels
import coil3.ImageLoader
import coil3.request.ImageRequest
import coil3.request.SuccessResult
import coil3.request.error
import coil3.request.fallback
import coil3.request.lifecycle
import coil3.request.placeholder
import coil3.request.target
import coil3.util.CoilUtils
import com.google.android.material.chip.Chip
import dagger.hilt.android.AndroidEntryPoint
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.nav.router
import org.koitharu.kotatsu.core.ui.BaseFragment
import org.koitharu.kotatsu.core.ui.image.CoverSizeResolver
import org.koitharu.kotatsu.core.ui.widgets.ChipsView
import org.koitharu.kotatsu.core.util.ext.crossfade
import org.koitharu.kotatsu.core.util.ext.defaultPlaceholders
import org.koitharu.kotatsu.core.util.ext.drawable
import org.koitharu.kotatsu.core.util.ext.enqueueWith
import org.koitharu.kotatsu.core.util.ext.mangaSourceExtra
import org.koitharu.kotatsu.core.util.ext.observe
import org.koitharu.kotatsu.core.util.ext.textAndVisible
import org.koitharu.kotatsu.databinding.FragmentPreviewBinding
@ -143,27 +129,7 @@ class PreviewFragment : BaseFragment<FragmentPreviewBinding>(), View.OnClickList
private fun loadCover(manga: Manga) {
val imageUrl = manga.largeCoverUrl.ifNullOrEmpty { manga.coverUrl }
val lastResult = CoilUtils.result(requireViewBinding().imageViewCover)
if (lastResult is SuccessResult && lastResult.request.data == imageUrl) {
return
}
val request = ImageRequest.Builder(context ?: return)
.target(requireViewBinding().imageViewCover)
.size(CoverSizeResolver(requireViewBinding().imageViewCover))
.data(imageUrl)
.mangaSourceExtra(manga.source)
.crossfade(requireContext())
.lifecycle(viewLifecycleOwner)
.placeholderMemoryCacheKey(manga.coverUrl)
val previousDrawable = lastResult?.drawable
if (previousDrawable != null) {
request.fallback(previousDrawable)
.placeholder(previousDrawable)
.error(previousDrawable)
} else {
request.defaultPlaceholders(requireContext())
}
request.enqueueWith(coil)
requireViewBinding().imageViewCover.setImageAsync(imageUrl, manga)
}
private fun onTagsChipsChanged(chips: List<ChipsView.ChipModel>) {

@ -15,6 +15,7 @@ import kotlin.math.roundToInt
class DynamicItemSizeResolver(
resources: Resources,
private val lifecycleOwner: LifecycleOwner,
private val settings: AppSettings,
private val adjustWidth: Boolean,
) : ItemSizeResolver {
@ -27,7 +28,6 @@ class DynamicItemSizeResolver(
get() = (gridWidth * scaleFactor).roundToInt()
override fun attachToView(
lifecycleOwner: LifecycleOwner,
view: View,
textView: TextView?,
progressView: ReadingProgressView?

@ -2,7 +2,6 @@ package org.koitharu.kotatsu.list.ui.size
import android.view.View
import android.widget.TextView
import androidx.lifecycle.LifecycleOwner
import org.koitharu.kotatsu.history.ui.util.ReadingProgressView
interface ItemSizeResolver {
@ -10,7 +9,6 @@ interface ItemSizeResolver {
val cellWidth: Int
fun attachToView(
lifecycleOwner: LifecycleOwner,
view: View,
textView: TextView?,
progressView: ReadingProgressView?,

@ -4,7 +4,6 @@ import android.view.View
import android.widget.TextView
import androidx.core.view.updateLayoutParams
import androidx.core.widget.TextViewCompat
import androidx.lifecycle.LifecycleOwner
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.history.ui.util.ReadingProgressView
@ -16,7 +15,6 @@ class StaticItemSizeResolver(
private var textAppearanceResId = R.style.TextAppearance_Kotatsu_GridTitle
override fun attachToView(
lifecycleOwner: LifecycleOwner,
view: View,
textView: TextView?,
progressView: ReadingProgressView?

@ -1,18 +1,17 @@
package org.koitharu.kotatsu.reader.ui.colorfilter
import android.content.res.Resources
import android.graphics.Bitmap
import android.os.Bundle
import android.view.View
import android.widget.CompoundButton
import android.widget.ImageView
import androidx.activity.viewModels
import androidx.core.view.WindowInsetsCompat
import coil3.ImageLoader
import coil3.asDrawable
import coil3.request.ErrorResult
import coil3.request.ImageRequest
import coil3.request.bitmapConfig
import coil3.request.error
import coil3.size.Scale
import coil3.size.ViewSizeResolver
import coil3.request.SuccessResult
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.slider.LabelFormatter
import com.google.android.material.slider.Slider
@ -20,19 +19,15 @@ import dagger.hilt.android.AndroidEntryPoint
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.ui.BaseActivity
import org.koitharu.kotatsu.core.util.ext.consumeAllSystemBarsInsets
import org.koitharu.kotatsu.core.util.ext.decodeRegion
import org.koitharu.kotatsu.core.util.ext.enqueueWith
import org.koitharu.kotatsu.core.util.ext.indicator
import org.koitharu.kotatsu.core.util.ext.mangaSourceExtra
import org.koitharu.kotatsu.core.util.ext.observe
import org.koitharu.kotatsu.core.util.ext.observeEvent
import org.koitharu.kotatsu.core.util.ext.setChecked
import org.koitharu.kotatsu.core.util.ext.setValueRounded
import org.koitharu.kotatsu.core.util.ext.systemBarsInsets
import org.koitharu.kotatsu.core.util.progress.ImageRequestIndicatorListener
import org.koitharu.kotatsu.databinding.ActivityColorFilterBinding
import org.koitharu.kotatsu.parsers.model.MangaPage
import org.koitharu.kotatsu.parsers.util.format
import org.koitharu.kotatsu.parsers.util.nullIfEmpty
import org.koitharu.kotatsu.reader.domain.ReaderColorFilter
import javax.inject.Inject
@ -50,7 +45,7 @@ class ColorFilterConfigActivity :
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(ActivityColorFilterBinding.inflate(layoutInflater))
setDisplayHomeAsUp(true, true)
setDisplayHomeAsUp(isEnabled = true, showUpAsClose = true)
viewBinding.sliderBrightness.addOnChangeListener(this)
viewBinding.sliderContrast.addOnChangeListener(this)
val formatter = PercentLabelFormatter(resources)
@ -128,19 +123,17 @@ class ColorFilterConfigActivity :
viewBinding.imageViewAfter.colorFilter = readerColorFilter?.toColorFilter()
}
private fun loadPreview(page: MangaPage) {
val data: Any = page.preview?.nullIfEmpty() ?: page
ImageRequest.Builder(this@ColorFilterConfigActivity)
.data(data)
.scale(Scale.FILL)
.decodeRegion()
.mangaSourceExtra(page.source)
.bitmapConfig(if (viewModel.is32BitColorsEnabled) Bitmap.Config.ARGB_8888 else Bitmap.Config.RGB_565)
.indicator(listOf(viewBinding.progressBefore, viewBinding.progressAfter))
.error(R.drawable.ic_error_placeholder)
.size(ViewSizeResolver(viewBinding.imageViewBefore))
.target(DoubleViewTarget(viewBinding.imageViewBefore, viewBinding.imageViewAfter))
.enqueueWith(coil)
private fun loadPreview(page: MangaPage) = with(viewBinding.imageViewBefore) {
addImageRequestListener(
ImageRequestIndicatorListener(
listOf(
viewBinding.progressBefore,
viewBinding.progressAfter,
),
),
)
addImageRequestListener(ShadowImageListener(viewBinding.imageViewAfter))
setImageAsync(page)
}
private fun onLoadingChanged(isLoading: Boolean) {
@ -160,4 +153,24 @@ class ColorFilterConfigActivity :
return pattern.format(percent)
}
}
private class ShadowImageListener(
private val imageView: ImageView
) : ImageRequest.Listener {
override fun onError(request: ImageRequest, result: ErrorResult) {
super.onError(request, result)
imageView.setImageDrawable(result.image?.asDrawable(imageView.resources))
}
override fun onStart(request: ImageRequest) {
super.onStart(request)
imageView.setImageDrawable(request.placeholder()?.asDrawable(imageView.resources))
}
override fun onSuccess(request: ImageRequest, result: SuccessResult) {
super.onSuccess(request, result)
imageView.setImageDrawable(result.image.asDrawable(imageView.resources))
}
}
}

@ -1,18 +0,0 @@
package org.koitharu.kotatsu.reader.ui.colorfilter
import android.graphics.drawable.Drawable
import android.widget.ImageView
import coil3.target.ImageViewTarget
class DoubleViewTarget(
primaryView: ImageView,
private val secondaryView: ImageView,
) : ImageViewTarget(primaryView) {
override var drawable: Drawable?
get() = super.drawable
set(value) {
super.drawable = value
secondaryView.setImageDrawable(value?.constantState?.newDrawable())
}
}

@ -7,9 +7,6 @@ import androidx.activity.viewModels
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.updatePadding
import coil3.ImageLoader
import coil3.request.error
import coil3.request.fallback
import coil3.request.placeholder
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import dagger.hilt.android.AndroidEntryPoint
import org.koitharu.kotatsu.R
@ -18,9 +15,6 @@ import org.koitharu.kotatsu.core.nav.router
import org.koitharu.kotatsu.core.ui.BaseActivity
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.util.ext.consumeAllSystemBarsInsets
import org.koitharu.kotatsu.core.util.ext.disposeImageRequest
import org.koitharu.kotatsu.core.util.ext.enqueueWith
import org.koitharu.kotatsu.core.util.ext.newImageRequest
import org.koitharu.kotatsu.core.util.ext.observe
import org.koitharu.kotatsu.core.util.ext.observeEvent
import org.koitharu.kotatsu.core.util.ext.showOrHide
@ -113,15 +107,11 @@ class ScrobblerConfigActivity : BaseActivity<ActivityScrobblerConfigBinding>(),
private fun onUserChanged(user: ScrobblerUser?) {
if (user == null) {
viewBinding.imageViewAvatar.disposeImageRequest()
viewBinding.imageViewAvatar.disposeImage()
viewBinding.imageViewAvatar.setImageResource(appcompatR.drawable.abc_ic_menu_overflow_material)
return
}
viewBinding.imageViewAvatar.newImageRequest(this, user.avatar)
?.placeholder(R.drawable.bg_badge_empty)
?.fallback(R.drawable.ic_shortcut_default)
?.error(R.drawable.ic_shortcut_default)
?.enqueueWith(coil)
viewBinding.imageViewAvatar.setImageAsync(user.avatar)
}
private fun onLoadingStateChanged(isLoading: Boolean) {

@ -1,21 +1,14 @@
package org.koitharu.kotatsu.scrobbling.common.ui.config.adapter
import androidx.lifecycle.LifecycleOwner
import coil3.ImageLoader
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
import org.koitharu.kotatsu.core.ui.list.AdapterDelegateClickListenerAdapter
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.util.ext.defaultPlaceholders
import org.koitharu.kotatsu.core.util.ext.enqueueWith
import org.koitharu.kotatsu.core.util.ext.newImageRequest
import org.koitharu.kotatsu.databinding.ItemScrobblingMangaBinding
import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblingInfo
fun scrobblingMangaAD(
clickListener: OnListItemClickListener<ScrobblingInfo>,
coil: ImageLoader,
lifecycleOwner: LifecycleOwner,
) = adapterDelegateViewBinding<ScrobblingInfo, ListModel, ItemScrobblingMangaBinding>(
{ layoutInflater, parent -> ItemScrobblingMangaBinding.inflate(layoutInflater, parent, false) },
) {
@ -23,10 +16,7 @@ fun scrobblingMangaAD(
AdapterDelegateClickListenerAdapter(this, clickListener).attach(itemView)
bind {
binding.imageViewCover.newImageRequest(lifecycleOwner, item.coverUrl)?.run {
defaultPlaceholders(context)
enqueueWith(coil)
}
binding.imageViewCover.setImageAsync(item.coverUrl, null)
binding.textViewTitle.text = item.title
binding.ratingBar.rating = item.rating * binding.ratingBar.numStars
}

@ -17,7 +17,7 @@ class ScrobblingMangaAdapter(
init {
addDelegate(ListItemType.HEADER, scrobblingHeaderAD())
addDelegate(ListItemType.STATE_EMPTY, emptyStateListAD(coil, lifecycleOwner, null))
addDelegate(ListItemType.MANGA_SCROBBLING, scrobblingMangaAD(clickListener, coil, lifecycleOwner))
addDelegate(ListItemType.STATE_EMPTY, emptyStateListAD(null))
addDelegate(ListItemType.MANGA_SCROBBLING, scrobblingMangaAD(clickListener))
}
}

@ -20,7 +20,7 @@ class ScrobblerSelectorAdapter(
init {
addDelegate(ListItemType.STATE_LOADING, loadingStateAD())
addDelegate(ListItemType.MANGA_SCROBBLING, scrobblingMangaAD(lifecycleOwner, coil, clickListener))
addDelegate(ListItemType.MANGA_SCROBBLING, scrobblingMangaAD(clickListener))
addDelegate(ListItemType.FOOTER_LOADING, loadingFooterAD())
addDelegate(ListItemType.HINT_EMPTY, scrobblerHintAD(stateHolderListener))
}

@ -1,21 +1,13 @@
package org.koitharu.kotatsu.scrobbling.common.ui.selector.adapter
import androidx.lifecycle.LifecycleOwner
import coil3.ImageLoader
import coil3.request.allowRgb565
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.util.ext.defaultPlaceholders
import org.koitharu.kotatsu.core.util.ext.enqueueWith
import org.koitharu.kotatsu.core.util.ext.newImageRequest
import org.koitharu.kotatsu.core.util.ext.textAndVisible
import org.koitharu.kotatsu.databinding.ItemMangaListBinding
import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblerManga
fun scrobblingMangaAD(
lifecycleOwner: LifecycleOwner,
coil: ImageLoader,
clickListener: OnListItemClickListener<ScrobblerManga>,
) = adapterDelegateViewBinding<ScrobblerManga, ListModel, ItemMangaListBinding>(
{ inflater, parent -> ItemMangaListBinding.inflate(inflater, parent, false) },
@ -27,10 +19,6 @@ fun scrobblingMangaAD(
bind {
binding.textViewTitle.text = item.name
binding.textViewSubtitle.textAndVisible = item.altName
binding.imageViewCover.newImageRequest(lifecycleOwner, item.cover)?.run {
defaultPlaceholders(context)
allowRgb565(true)
enqueueWith(coil)
}
binding.imageViewCover.setImageAsync(item.cover, null)
}
}

@ -76,7 +76,7 @@ class SearchActivity :
router.openList(item.source, item.listFilter, item.sortOrder)
}
}
val sizeResolver = DynamicItemSizeResolver(resources, settings, adjustWidth = true)
val sizeResolver = DynamicItemSizeResolver(resources, this, settings, adjustWidth = true)
val selectionDecoration = MangaSelectionDecoration(this)
selectionController = ListSelectionController(
appCompatDelegate = delegate,

@ -34,8 +34,6 @@ class SearchAdapter(
ListItemType.MANGA_NESTED_GROUP,
searchResultsAD(
sharedPool = pool,
lifecycleOwner = lifecycleOwner,
coil = coil,
sizeResolver = sizeResolver,
selectionDecoration = selectionDecoration,
listener = listener,
@ -44,7 +42,7 @@ class SearchAdapter(
)
addDelegate(ListItemType.STATE_LOADING, loadingStateAD())
addDelegate(ListItemType.FOOTER_LOADING, loadingFooterAD())
addDelegate(ListItemType.STATE_EMPTY, emptyStateListAD(coil, lifecycleOwner, listener))
addDelegate(ListItemType.STATE_EMPTY, emptyStateListAD(listener))
addDelegate(ListItemType.STATE_ERROR, errorStateListAD(listener))
addDelegate(ListItemType.FOOTER_BUTTON, buttonFooterAD(listener))
}

@ -2,9 +2,7 @@ package org.koitharu.kotatsu.search.ui.multi.adapter
import androidx.core.view.isGone
import androidx.core.view.isVisible
import androidx.lifecycle.LifecycleOwner
import androidx.recyclerview.widget.RecyclerView.RecycledViewPool
import coil3.ImageLoader
import com.hannesdorfmann.adapterdelegates4.ListDelegationAdapter
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
import org.koitharu.kotatsu.R
@ -24,8 +22,6 @@ import org.koitharu.kotatsu.search.ui.multi.SearchResultsListModel
fun searchResultsAD(
sharedPool: RecycledViewPool,
lifecycleOwner: LifecycleOwner,
coil: ImageLoader,
sizeResolver: ItemSizeResolver,
selectionDecoration: MangaSelectionDecoration,
listener: OnListItemClickListener<Manga>,
@ -35,9 +31,7 @@ fun searchResultsAD(
) {
binding.recyclerView.setRecycledViewPool(sharedPool)
val adapter = ListDelegationAdapter(
mangaGridItemAD(coil, lifecycleOwner, sizeResolver, listener),
)
val adapter = ListDelegationAdapter(mangaGridItemAD(sizeResolver, listener))
binding.recyclerView.addItemDecoration(selectionDecoration)
binding.recyclerView.adapter = adapter
val spacing = context.resources.getDimensionPixelOffset(R.dimen.grid_spacing_outer)

@ -17,10 +17,10 @@ class SearchSuggestionAdapter(
init {
delegatesManager
.addDelegate(SEARCH_SUGGESTION_ITEM_TYPE_QUERY, searchSuggestionQueryAD(listener))
.addDelegate(searchSuggestionSourceAD(coil, lifecycleOwner, listener))
.addDelegate(searchSuggestionSourceTipAD(coil, lifecycleOwner, listener))
.addDelegate(searchSuggestionSourceAD(listener))
.addDelegate(searchSuggestionSourceTipAD(listener))
.addDelegate(searchSuggestionTagsAD(listener))
.addDelegate(searchSuggestionMangaListAD(coil, lifecycleOwner, listener))
.addDelegate(searchSuggestionMangaListAD(listener))
.addDelegate(searchSuggestionQueryHintAD(listener))
.addDelegate(searchSuggestionAuthorAD(listener))
}

@ -1,27 +1,13 @@
package org.koitharu.kotatsu.search.ui.suggestion.adapter
import androidx.lifecycle.LifecycleOwner
import coil3.ImageLoader
import coil3.request.error
import coil3.request.fallback
import coil3.request.placeholder
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.model.getSummary
import org.koitharu.kotatsu.core.model.getTitle
import org.koitharu.kotatsu.core.parser.favicon.faviconUri
import org.koitharu.kotatsu.core.ui.image.AnimatedFaviconDrawable
import org.koitharu.kotatsu.core.ui.image.FaviconDrawable
import org.koitharu.kotatsu.core.util.ext.enqueueWith
import org.koitharu.kotatsu.core.util.ext.mangaSourceExtra
import org.koitharu.kotatsu.core.util.ext.newImageRequest
import org.koitharu.kotatsu.databinding.ItemSearchSuggestionSourceBinding
import org.koitharu.kotatsu.search.ui.suggestion.SearchSuggestionListener
import org.koitharu.kotatsu.search.ui.suggestion.model.SearchSuggestionItem
fun searchSuggestionSourceAD(
coil: ImageLoader,
lifecycleOwner: LifecycleOwner,
listener: SearchSuggestionListener,
) = adapterDelegateViewBinding<SearchSuggestionItem.Source, SearchSuggestionItem, ItemSearchSuggestionSourceBinding>(
{ inflater, parent -> ItemSearchSuggestionSourceBinding.inflate(inflater, parent, false) },
@ -38,13 +24,6 @@ fun searchSuggestionSourceAD(
binding.textViewTitle.text = item.source.getTitle(context)
binding.textViewSubtitle.text = item.source.getSummary(context)
binding.switchLocal.isChecked = item.isEnabled
val fallbackIcon = FaviconDrawable(context, R.style.FaviconDrawable_Small, item.source.name)
binding.imageViewCover.newImageRequest(lifecycleOwner, item.source.faviconUri())?.run {
fallback(fallbackIcon)
placeholder(AnimatedFaviconDrawable(context, R.style.FaviconDrawable_Small, item.source.name))
error(fallbackIcon)
mangaSourceExtra(item.source)
enqueueWith(coil)
}
binding.imageViewCover.setImageAsync(item.source)
}
}

@ -1,27 +1,13 @@
package org.koitharu.kotatsu.search.ui.suggestion.adapter
import androidx.lifecycle.LifecycleOwner
import coil3.ImageLoader
import coil3.request.error
import coil3.request.fallback
import coil3.request.placeholder
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.model.getSummary
import org.koitharu.kotatsu.core.model.getTitle
import org.koitharu.kotatsu.core.parser.favicon.faviconUri
import org.koitharu.kotatsu.core.ui.image.AnimatedFaviconDrawable
import org.koitharu.kotatsu.core.ui.image.FaviconDrawable
import org.koitharu.kotatsu.core.util.ext.enqueueWith
import org.koitharu.kotatsu.core.util.ext.mangaSourceExtra
import org.koitharu.kotatsu.core.util.ext.newImageRequest
import org.koitharu.kotatsu.databinding.ItemSearchSuggestionSourceTipBinding
import org.koitharu.kotatsu.search.ui.suggestion.SearchSuggestionListener
import org.koitharu.kotatsu.search.ui.suggestion.model.SearchSuggestionItem
fun searchSuggestionSourceTipAD(
coil: ImageLoader,
lifecycleOwner: LifecycleOwner,
listener: SearchSuggestionListener,
) =
adapterDelegateViewBinding<SearchSuggestionItem.SourceTip, SearchSuggestionItem, ItemSearchSuggestionSourceTipBinding>(
@ -35,13 +21,6 @@ fun searchSuggestionSourceTipAD(
bind {
binding.textViewTitle.text = item.source.getTitle(context)
binding.textViewSubtitle.text = item.source.getSummary(context)
val fallbackIcon = FaviconDrawable(context, R.style.FaviconDrawable_Small, item.source.name)
binding.imageViewCover.newImageRequest(lifecycleOwner, item.source.faviconUri())?.run {
fallback(fallbackIcon)
placeholder(AnimatedFaviconDrawable(context, R.style.FaviconDrawable_Small, item.source.name))
error(fallbackIcon)
mangaSourceExtra(item.source)
enqueueWith(coil)
}
binding.imageViewCover.setImageAsync(item.source)
}
}

@ -1,36 +1,25 @@
package org.koitharu.kotatsu.search.ui.suggestion.adapter
import androidx.core.view.updatePadding
import androidx.lifecycle.LifecycleOwner
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
import coil3.ImageLoader
import coil3.request.allowRgb565
import coil3.request.transformations
import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegate
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.ui.image.TrimTransformation
import org.koitharu.kotatsu.core.ui.list.decor.SpacingItemDecoration
import org.koitharu.kotatsu.core.util.RecyclerViewScrollCallback
import org.koitharu.kotatsu.core.util.ext.defaultPlaceholders
import org.koitharu.kotatsu.core.util.ext.enqueueWith
import org.koitharu.kotatsu.core.util.ext.mangaSourceExtra
import org.koitharu.kotatsu.core.util.ext.newImageRequest
import org.koitharu.kotatsu.databinding.ItemSearchSuggestionMangaGridBinding
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.search.ui.suggestion.SearchSuggestionListener
import org.koitharu.kotatsu.search.ui.suggestion.model.SearchSuggestionItem
fun searchSuggestionMangaListAD(
coil: ImageLoader,
lifecycleOwner: LifecycleOwner,
listener: SearchSuggestionListener,
) = adapterDelegate<SearchSuggestionItem.MangaList, SearchSuggestionItem>(R.layout.item_search_suggestion_manga_list) {
val adapter = AsyncListDifferDelegationAdapter(
SuggestionMangaDiffCallback(),
searchSuggestionMangaGridAD(coil, lifecycleOwner, listener),
searchSuggestionMangaGridAD(listener),
)
val recyclerView = itemView as RecyclerView
recyclerView.adapter = adapter
@ -48,8 +37,6 @@ fun searchSuggestionMangaListAD(
}
private fun searchSuggestionMangaGridAD(
coil: ImageLoader,
lifecycleOwner: LifecycleOwner,
listener: SearchSuggestionListener,
) = adapterDelegateViewBinding<Manga, Manga, ItemSearchSuggestionMangaGridBinding>(
{ layoutInflater, parent -> ItemSearchSuggestionMangaGridBinding.inflate(layoutInflater, parent, false) },
@ -59,13 +46,7 @@ private fun searchSuggestionMangaGridAD(
}
bind {
binding.imageViewCover.newImageRequest(lifecycleOwner, item.coverUrl)?.run {
defaultPlaceholders(context)
allowRgb565(true)
transformations(TrimTransformation())
mangaSourceExtra(item.source)
enqueueWith(coil)
}
binding.imageViewCover.setImageAsync(item.coverUrl, item.source)
binding.textViewTitle.text = item.title
}
}

@ -9,23 +9,15 @@ import androidx.activity.viewModels
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.isVisible
import coil3.ImageLoader
import coil3.request.ImageRequest
import coil3.request.lifecycle
import coil3.request.target
import coil3.size.Scale
import com.google.android.material.snackbar.Snackbar
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.flow.filterNotNull
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.ui.BaseActivity
import org.koitharu.kotatsu.core.ui.image.CoverSizeResolver
import org.koitharu.kotatsu.core.ui.model.MangaOverride
import org.koitharu.kotatsu.core.util.ext.consumeAll
import org.koitharu.kotatsu.core.util.ext.crossfade
import org.koitharu.kotatsu.core.util.ext.enqueueWith
import org.koitharu.kotatsu.core.util.ext.getDisplayMessage
import org.koitharu.kotatsu.core.util.ext.getThemeColor
import org.koitharu.kotatsu.core.util.ext.mangaSourceExtra
import org.koitharu.kotatsu.core.util.ext.observe
import org.koitharu.kotatsu.core.util.ext.observeEvent
import org.koitharu.kotatsu.core.util.ext.tryLaunch
@ -107,15 +99,7 @@ class OverrideConfigActivity : BaseActivity<ActivityOverrideEditBinding>(), View
private fun onDataChanged(data: Pair<Manga, MangaOverride>) {
val (manga, override) = data
ImageRequest.Builder(this)
.target(viewBinding.imageViewCover)
.size(CoverSizeResolver(viewBinding.imageViewCover))
.scale(Scale.FILL)
.data(override.coverUrl.ifNullOrEmpty { manga.coverUrl })
.mangaSourceExtra(manga.source)
.crossfade(this)
.lifecycle(this)
.enqueueWith(coil)
viewBinding.imageViewCover.setImageAsync(override.coverUrl.ifNullOrEmpty { manga.coverUrl }, manga)
viewBinding.layoutName.placeholderText = manga.title
if (viewBinding.editName.tag == null) {
viewBinding.editName.setText(override.title)

@ -13,7 +13,7 @@ class SourceConfigAdapter(
init {
with(delegatesManager) {
addDelegate(sourceConfigItemDelegate2(listener, coil, lifecycleOwner))
addDelegate(sourceConfigItemDelegate2(listener))
addDelegate(sourceConfigEmptySearchDelegate())
addDelegate(sourceConfigTipDelegate(listener))
}

@ -6,33 +6,19 @@ import androidx.core.content.ContextCompat
import androidx.core.content.pm.ShortcutManagerCompat
import androidx.core.view.isGone
import androidx.core.view.isVisible
import androidx.lifecycle.LifecycleOwner
import coil3.ImageLoader
import coil3.request.error
import coil3.request.fallback
import coil3.request.placeholder
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegate
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.model.getSummary
import org.koitharu.kotatsu.core.model.getTitle
import org.koitharu.kotatsu.core.parser.favicon.faviconUri
import org.koitharu.kotatsu.core.ui.image.AnimatedFaviconDrawable
import org.koitharu.kotatsu.core.ui.image.FaviconDrawable
import org.koitharu.kotatsu.core.ui.list.OnTipCloseListener
import org.koitharu.kotatsu.core.util.ext.crossfade
import org.koitharu.kotatsu.core.util.ext.drawableStart
import org.koitharu.kotatsu.core.util.ext.enqueueWith
import org.koitharu.kotatsu.core.util.ext.mangaSourceExtra
import org.koitharu.kotatsu.core.util.ext.newImageRequest
import org.koitharu.kotatsu.databinding.ItemSourceConfigBinding
import org.koitharu.kotatsu.databinding.ItemTipBinding
import org.koitharu.kotatsu.settings.sources.model.SourceConfigItem
fun sourceConfigItemDelegate2(
listener: SourceConfigListener,
coil: ImageLoader,
lifecycleOwner: LifecycleOwner,
) = adapterDelegateViewBinding<SourceConfigItem.SourceItem, SourceConfigItem, ItemSourceConfigBinding>(
{ layoutInflater, parent ->
ItemSourceConfigBinding.inflate(
@ -62,15 +48,7 @@ fun sourceConfigItemDelegate2(
binding.imageViewMenu.isVisible = item.isEnabled
binding.textViewTitle.drawableStart = if (item.isPinned) iconPinned else null
binding.textViewDescription.text = item.source.getSummary(context)
val fallbackIcon = FaviconDrawable(context, R.style.FaviconDrawable_Small, item.source.name)
binding.imageViewIcon.newImageRequest(lifecycleOwner, item.source.faviconUri())?.run {
crossfade(context)
error(fallbackIcon)
placeholder(AnimatedFaviconDrawable(context, R.style.FaviconDrawable_Small, item.source.name))
fallback(fallbackIcon)
mangaSourceExtra(item.source)
enqueueWith(coil)
}
binding.imageViewIcon.setImageAsync(item.source)
}
}

@ -3,26 +3,14 @@ package org.koitharu.kotatsu.settings.sources.catalog
import androidx.core.content.ContextCompat
import androidx.core.view.isVisible
import androidx.core.view.updatePaddingRelative
import androidx.lifecycle.LifecycleOwner
import coil3.ImageLoader
import coil3.request.error
import coil3.request.fallback
import coil3.request.placeholder
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.browser.cloudflare.CaptchaNotifier.Companion.ignoreCaptchaErrors
import org.koitharu.kotatsu.core.model.getSummary
import org.koitharu.kotatsu.core.model.getTitle
import org.koitharu.kotatsu.core.parser.favicon.faviconUri
import org.koitharu.kotatsu.core.ui.image.AnimatedFaviconDrawable
import org.koitharu.kotatsu.core.ui.image.FaviconDrawable
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.util.ext.crossfade
import org.koitharu.kotatsu.core.util.ext.drawableStart
import org.koitharu.kotatsu.core.util.ext.enqueueWith
import org.koitharu.kotatsu.core.util.ext.getThemeDimensionPixelOffset
import org.koitharu.kotatsu.core.util.ext.mangaSourceExtra
import org.koitharu.kotatsu.core.util.ext.newImageRequest
import org.koitharu.kotatsu.core.util.ext.setTextAndVisible
import org.koitharu.kotatsu.databinding.ItemEmptyHintBinding
import org.koitharu.kotatsu.databinding.ItemSourceCatalogBinding
@ -30,8 +18,6 @@ import org.koitharu.kotatsu.list.ui.model.ListModel
import androidx.appcompat.R as appcompatR
fun sourceCatalogItemSourceAD(
coil: ImageLoader,
lifecycleOwner: LifecycleOwner,
listener: OnListItemClickListener<SourceCatalogItem.Source>
) = adapterDelegateViewBinding<SourceCatalogItem.Source, ListModel, ItemSourceCatalogBinding>(
{ layoutInflater, parent ->
@ -61,30 +47,19 @@ fun sourceCatalogItemSourceAD(
} else {
null
}
val fallbackIcon = FaviconDrawable(context, R.style.FaviconDrawable_Small, item.source.name)
binding.imageViewIcon.newImageRequest(lifecycleOwner, item.source.faviconUri())?.run {
crossfade(context)
error(fallbackIcon)
placeholder(AnimatedFaviconDrawable(context, R.style.FaviconDrawable_Small, item.source.name))
fallback(fallbackIcon)
mangaSourceExtra(item.source)
ignoreCaptchaErrors()
enqueueWith(coil)
}
FaviconDrawable(context, R.style.FaviconDrawable_Small, item.source.name)
binding.imageViewIcon.setImageAsync(item.source)
}
}
fun sourceCatalogItemHintAD(
coil: ImageLoader,
lifecycleOwner: LifecycleOwner,
) = adapterDelegateViewBinding<SourceCatalogItem.Hint, ListModel, ItemEmptyHintBinding>(
fun sourceCatalogItemHintAD() = adapterDelegateViewBinding<SourceCatalogItem.Hint, ListModel, ItemEmptyHintBinding>(
{ inflater, parent -> ItemEmptyHintBinding.inflate(inflater, parent, false) },
) {
binding.buttonRetry.isVisible = false
bind {
binding.icon.newImageRequest(lifecycleOwner, item.icon)?.enqueueWith(coil)
binding.icon.setImageAsync(item.icon)
binding.textPrimary.setText(item.title)
binding.textSecondary.setTextAndVisible(item.text)
}

@ -10,7 +10,6 @@ import androidx.appcompat.widget.SearchView
import androidx.core.graphics.Insets
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.updatePadding
import coil3.ImageLoader
import com.google.android.material.appbar.AppBarLayout
import com.google.android.material.chip.Chip
import dagger.hilt.android.AndroidEntryPoint
@ -33,7 +32,6 @@ import org.koitharu.kotatsu.databinding.ActivitySourcesCatalogBinding
import org.koitharu.kotatsu.list.ui.adapter.TypedListSpacingDecoration
import org.koitharu.kotatsu.main.ui.owners.AppBarOwner
import org.koitharu.kotatsu.parsers.model.ContentType
import javax.inject.Inject
@AndroidEntryPoint
class SourcesCatalogActivity : BaseActivity<ActivitySourcesCatalogBinding>(),
@ -42,9 +40,6 @@ class SourcesCatalogActivity : BaseActivity<ActivitySourcesCatalogBinding>(),
MenuItem.OnActionExpandListener,
ChipsView.OnChipClickListener {
@Inject
lateinit var coil: ImageLoader
override val appBar: AppBarLayout
get() = viewBinding.appbar
@ -53,8 +48,8 @@ class SourcesCatalogActivity : BaseActivity<ActivitySourcesCatalogBinding>(),
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(ActivitySourcesCatalogBinding.inflate(layoutInflater))
setDisplayHomeAsUp(true, false)
val sourcesAdapter = SourcesCatalogAdapter(this, coil, this)
setDisplayHomeAsUp(isEnabled = true, showUpAsClose = false)
val sourcesAdapter = SourcesCatalogAdapter(this)
with(viewBinding.recyclerView) {
setHasFixedSize(true)
addItemDecoration(TypedListSpacingDecoration(context, false))
@ -85,7 +80,7 @@ class SourcesCatalogActivity : BaseActivity<ActivitySourcesCatalogBinding>(),
right = bars.right,
top = bars.top,
)
return return WindowInsetsCompat.Builder(insets)
return WindowInsetsCompat.Builder(insets)
.setInsets(WindowInsetsCompat.Type.systemBars(), Insets.NONE)
.build()
}

@ -1,8 +1,6 @@
package org.koitharu.kotatsu.settings.sources.catalog
import android.content.Context
import androidx.lifecycle.LifecycleOwner
import coil3.ImageLoader
import org.koitharu.kotatsu.core.model.getTitle
import org.koitharu.kotatsu.core.ui.BaseListAdapter
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
@ -13,13 +11,11 @@ import org.koitharu.kotatsu.list.ui.model.ListModel
class SourcesCatalogAdapter(
listener: OnListItemClickListener<SourceCatalogItem.Source>,
coil: ImageLoader,
lifecycleOwner: LifecycleOwner,
) : BaseListAdapter<ListModel>(), FastScroller.SectionIndexer {
init {
addDelegate(ListItemType.CHAPTER_LIST, sourceCatalogItemSourceAD(coil, lifecycleOwner, listener))
addDelegate(ListItemType.HINT_EMPTY, sourceCatalogItemHintAD(coil, lifecycleOwner))
addDelegate(ListItemType.CHAPTER_LIST, sourceCatalogItemSourceAD(listener))
addDelegate(ListItemType.HINT_EMPTY, sourceCatalogItemHintAD())
addDelegate(ListItemType.STATE_LOADING, loadingStateAD())
}

@ -30,8 +30,6 @@ import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.ui.util.ReversibleActionObserver
import org.koitharu.kotatsu.core.util.KotatsuColors
import org.koitharu.kotatsu.core.util.ext.end
import org.koitharu.kotatsu.core.util.ext.enqueueWith
import org.koitharu.kotatsu.core.util.ext.newImageRequest
import org.koitharu.kotatsu.core.util.ext.observe
import org.koitharu.kotatsu.core.util.ext.observeEvent
import org.koitharu.kotatsu.core.util.ext.setTextAndVisible
@ -125,7 +123,7 @@ class StatsActivity : BaseActivity<ActivityStatsBinding>(),
marginStart = baseMargin + bars.start(v)
marginEnd = if (isTablet) baseMargin else baseMargin + bars.end(v)
}
return return WindowInsetsCompat.Builder(insets)
return WindowInsetsCompat.Builder(insets)
.setInsets(WindowInsetsCompat.Type.systemBars(), Insets.NONE)
.build()
}
@ -177,7 +175,7 @@ class StatsActivity : BaseActivity<ActivityStatsBinding>(),
override fun onInflate(stub: ViewStub?, inflated: View) {
val stubBinding = ItemEmptyStateBinding.bind(inflated)
stubBinding.icon.newImageRequest(this, R.drawable.ic_empty_history)?.enqueueWith(coil)
stubBinding.icon.setImageAsync(R.drawable.ic_empty_history)
stubBinding.textPrimary.setText(R.string.text_empty_holder_primary)
stubBinding.textSecondary.setTextAndVisible(R.string.empty_stats_text)
stubBinding.buttonRetry.isVisible = false

@ -6,25 +6,16 @@ import androidx.core.content.ContextCompat
import androidx.core.text.bold
import androidx.core.text.buildSpannedString
import androidx.core.text.color
import androidx.lifecycle.LifecycleOwner
import coil3.ImageLoader
import coil3.request.allowRgb565
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.util.ext.defaultPlaceholders
import org.koitharu.kotatsu.core.util.ext.drawableStart
import org.koitharu.kotatsu.core.util.ext.enqueueWith
import org.koitharu.kotatsu.core.util.ext.getThemeColor
import org.koitharu.kotatsu.core.util.ext.mangaSourceExtra
import org.koitharu.kotatsu.core.util.ext.newImageRequest
import org.koitharu.kotatsu.databinding.ItemTrackDebugBinding
import org.koitharu.kotatsu.tracker.data.TrackEntity
import androidx.appcompat.R as appcompatR
fun trackDebugAD(
lifecycleOwner: LifecycleOwner,
coil: ImageLoader,
clickListener: OnListItemClickListener<TrackDebugItem>,
) = adapterDelegateViewBinding<TrackDebugItem, TrackDebugItem, ItemTrackDebugBinding>(
{ layoutInflater, parent -> ItemTrackDebugBinding.inflate(layoutInflater, parent, false) },
@ -36,12 +27,7 @@ fun trackDebugAD(
}
bind {
binding.imageViewCover.newImageRequest(lifecycleOwner, item.manga.coverUrl)?.run {
defaultPlaceholders(context)
allowRgb565(true)
mangaSourceExtra(item.manga.source)
enqueueWith(coil)
}
binding.imageViewCover.setImageAsync(item.manga.coverUrl, item.manga)
binding.textViewTitle.text = item.manga.title
binding.textViewSummary.text = buildSpannedString {
append(

@ -32,7 +32,7 @@ class TrackerDebugActivity : BaseActivity<ActivityTrackerDebugBinding>(), OnList
setContentView(ActivityTrackerDebugBinding.inflate(layoutInflater))
setDisplayHomeAsUp(true, false)
val tracksAdapter = BaseListAdapter<TrackDebugItem>()
.addDelegate(ListItemType.FEED, trackDebugAD(this, coil, this))
.addDelegate(ListItemType.FEED, trackDebugAD(this))
with(viewBinding.recyclerView) {
setHasFixedSize(true)
adapter = tracksAdapter

@ -61,7 +61,7 @@ class FeedFragment :
override fun onViewBindingCreated(binding: FragmentListBinding, savedInstanceState: Bundle?) {
super.onViewBindingCreated(binding, savedInstanceState)
val sizeResolver = StaticItemSizeResolver(resources.getDimensionPixelSize(R.dimen.smaller_grid_width))
val feedAdapter = FeedAdapter(coil, viewLifecycleOwner, this, sizeResolver) { item, v ->
val feedAdapter = FeedAdapter(this, sizeResolver) { item, v ->
viewModel.onItemClick(item)
onItemClick(item.manga, v)
}

@ -1,8 +1,6 @@
package org.koitharu.kotatsu.tracker.ui.feed.adapter
import android.content.Context
import androidx.lifecycle.LifecycleOwner
import coil3.ImageLoader
import org.koitharu.kotatsu.core.ui.BaseListAdapter
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.ui.list.fastscroll.FastScroller
@ -20,20 +18,16 @@ import org.koitharu.kotatsu.list.ui.size.ItemSizeResolver
import org.koitharu.kotatsu.tracker.ui.feed.model.FeedItem
class FeedAdapter(
coil: ImageLoader,
lifecycleOwner: LifecycleOwner,
listener: MangaListListener,
sizeResolver: ItemSizeResolver,
feedClickListener: OnListItemClickListener<FeedItem>,
) : BaseListAdapter<ListModel>(), FastScroller.SectionIndexer {
init {
addDelegate(ListItemType.FEED, feedItemAD(coil, lifecycleOwner, feedClickListener))
addDelegate(ListItemType.FEED, feedItemAD(feedClickListener))
addDelegate(
ListItemType.MANGA_NESTED_GROUP,
updatedMangaAD(
lifecycleOwner = lifecycleOwner,
coil = coil,
sizeResolver = sizeResolver,
listener = listener,
headerClickListener = listener,
@ -44,7 +38,7 @@ class FeedAdapter(
addDelegate(ListItemType.FOOTER_ERROR, errorFooterAD(listener))
addDelegate(ListItemType.STATE_ERROR, errorStateListAD(listener))
addDelegate(ListItemType.HEADER, listHeaderAD(listener))
addDelegate(ListItemType.STATE_EMPTY, emptyStateListAD(coil, lifecycleOwner, listener))
addDelegate(ListItemType.STATE_EMPTY, emptyStateListAD(listener))
addDelegate(ListItemType.QUICK_FILTER, quickFilterAD(listener))
}

@ -1,25 +1,16 @@
package org.koitharu.kotatsu.tracker.ui.feed.adapter
import androidx.core.content.ContextCompat
import androidx.lifecycle.LifecycleOwner
import coil3.ImageLoader
import coil3.request.allowRgb565
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.util.ext.defaultPlaceholders
import org.koitharu.kotatsu.core.util.ext.drawableStart
import org.koitharu.kotatsu.core.util.ext.enqueueWith
import org.koitharu.kotatsu.core.util.ext.getQuantityStringSafe
import org.koitharu.kotatsu.core.util.ext.mangaSourceExtra
import org.koitharu.kotatsu.core.util.ext.newImageRequest
import org.koitharu.kotatsu.databinding.ItemFeedBinding
import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.tracker.ui.feed.model.FeedItem
fun feedItemAD(
coil: ImageLoader,
lifecycleOwner: LifecycleOwner,
clickListener: OnListItemClickListener<FeedItem>,
) = adapterDelegateViewBinding<FeedItem, ListModel, ItemFeedBinding>(
{ inflater, parent -> ItemFeedBinding.inflate(inflater, parent, false) },
@ -31,12 +22,7 @@ fun feedItemAD(
}
bind {
binding.imageViewCover.newImageRequest(lifecycleOwner, item.imageUrl)?.run {
defaultPlaceholders(context)
allowRgb565(true)
mangaSourceExtra(item.manga.source)
enqueueWith(coil)
}
binding.imageViewCover.setImageAsync(item.imageUrl, item.manga.source)
binding.textViewTitle.text = item.title
binding.textViewSummary.text = context.resources.getQuantityStringSafe(
R.plurals.new_chapters,

@ -1,7 +1,5 @@
package org.koitharu.kotatsu.tracker.ui.feed.adapter
import androidx.lifecycle.LifecycleOwner
import coil3.ImageLoader
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.ui.BaseListAdapter
@ -17,8 +15,6 @@ import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.tracker.ui.feed.model.UpdatedMangaHeader
fun updatedMangaAD(
lifecycleOwner: LifecycleOwner,
coil: ImageLoader,
sizeResolver: ItemSizeResolver,
listener: OnListItemClickListener<Manga>,
headerClickListener: ListHeaderClickListener,
@ -27,7 +23,7 @@ fun updatedMangaAD(
) {
val adapter = BaseListAdapter<ListModel>()
.addDelegate(ListItemType.MANGA_GRID, mangaGridItemAD(coil, lifecycleOwner, sizeResolver, listener))
.addDelegate(ListItemType.MANGA_GRID, mangaGridItemAD(sizeResolver, listener))
binding.recyclerView.adapter = adapter
binding.buttonMore.setOnClickListener { v ->
headerClickListener.onListHeaderClick(ListHeader(0, payload = item), v)

@ -8,7 +8,7 @@
android:orientation="horizontal"
android:paddingHorizontal="32dp">
<ImageView
<org.koitharu.kotatsu.core.image.CoilImageView
android:id="@+id/icon"
android:layout_width="192dp"
android:layout_height="192dp"

@ -54,12 +54,14 @@
android:orientation="vertical"
app:layout_constraintGuide_percent="0.5" />
<com.google.android.material.imageview.ShapeableImageView
<org.koitharu.kotatsu.image.ui.CoverImageView
android:id="@+id/imageView_before"
android:layout_width="0dp"
android:layout_height="0dp"
android:padding="2dp"
android:scaleType="centerCrop"
app:allowRgb565="false"
app:decodeRegion="true"
app:layout_constraintDimensionRatio="W,14:9"
app:layout_constraintEnd_toStartOf="@id/imageView_arrow"
app:layout_constraintStart_toStartOf="parent"

@ -53,7 +53,7 @@
android:layout_height="match_parent"
android:paddingBottom="@dimen/margin_normal">
<com.google.android.material.imageview.ShapeableImageView
<org.koitharu.kotatsu.image.ui.CoverImageView
android:id="@+id/imageView_cover"
android:layout_width="0dp"
android:layout_height="0dp"
@ -63,6 +63,9 @@
android:clipToOutline="true"
android:foreground="?selectableItemBackground"
android:scaleType="centerCrop"
app:allowRgb565="false"
app:aspectRationHeight="0"
app:aspectRationWidth="0"
app:layout_constraintDimensionRatio="H,13:18"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0"
@ -70,6 +73,7 @@
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintWidth_percent="0.3"
app:shapeAppearanceOverlay="@style/ShapeAppearanceOverlay.Kotatsu.Cover"
app:useExistingDrawable="true"
tools:background="@tools:sample/backgrounds/scenic[5]"
tools:ignore="ContentDescription,UnusedAttribute" />

@ -38,12 +38,14 @@
android:orientation="vertical"
android:padding="@dimen/margin_normal">
<com.google.android.material.imageview.ShapeableImageView
<org.koitharu.kotatsu.image.ui.CoverImageView
android:id="@+id/imageView_before"
android:layout_width="0dp"
android:layout_height="0dp"
android:padding="2dp"
android:scaleType="centerCrop"
app:allowRgb565="false"
app:decodeRegion="true"
app:layout_constraintDimensionRatio="W,14:9"
app:layout_constraintEnd_toStartOf="@id/imageView_arrow"
app:layout_constraintStart_toStartOf="parent"

@ -45,7 +45,7 @@
android:layout_height="match_parent"
android:paddingBottom="@dimen/margin_normal">
<com.google.android.material.imageview.ShapeableImageView
<org.koitharu.kotatsu.image.ui.CoverImageView
android:id="@+id/imageView_cover"
android:layout_width="0dp"
android:layout_height="0dp"
@ -55,6 +55,9 @@
android:clipToOutline="true"
android:foreground="?selectableItemBackground"
android:scaleType="centerCrop"
app:allowRgb565="false"
app:aspectRationHeight="0"
app:aspectRationWidth="0"
app:layout_constraintDimensionRatio="H,13:18"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0"
@ -62,6 +65,7 @@
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintWidth_percent="0.3"
app:shapeAppearanceOverlay="@style/ShapeAppearanceOverlay.Kotatsu.Cover"
app:useExistingDrawable="true"
tools:background="@tools:sample/backgrounds/scenic[5]"
tools:ignore="ContentDescription,UnusedAttribute" />

@ -36,7 +36,7 @@
android:orientation="vertical"
android:paddingBottom="@dimen/screen_padding">
<com.google.android.material.imageview.ShapeableImageView
<org.koitharu.kotatsu.image.ui.CoverImageView
android:id="@+id/imageView_cover"
android:layout_width="0dp"
android:layout_height="0dp"

@ -26,7 +26,7 @@
android:layout_height="?attr/actionBarSize"
app:layout_collapseMode="pin">
<com.google.android.material.imageview.ShapeableImageView
<org.koitharu.kotatsu.core.image.CoilImageView
android:id="@+id/imageView_avatar"
android:layout_width="28dp"
android:layout_height="28dp"
@ -35,6 +35,9 @@
android:background="?selectableItemBackgroundBorderless"
android:padding="1dp"
android:scaleType="centerCrop"
app:errorDrawable="@drawable/ic_shortcut_default"
app:fallbackDrawable="@drawable/ic_shortcut_default"
app:placeholderDrawable="@drawable/bg_badge_empty"
app:shapeAppearanceOverlay="@style/ShapeAppearanceOverlay.Kotatsu.Circle"
app:strokeColor="?colorOutline"
app:strokeWidth="1dp"

@ -12,4 +12,4 @@
app:bubbleSize="small"
app:layoutManager="androidx.recyclerview.widget.GridLayoutManager"
app:spanCount="3"
tools:listitem="@layout/item_bookmark" />
tools:listitem="@layout/item_bookmark_large" />

@ -13,7 +13,7 @@
android:layout_height="wrap_content"
android:paddingBottom="?actionBarSize">
<com.google.android.material.imageview.ShapeableImageView
<org.koitharu.kotatsu.image.ui.CoverImageView
android:id="@+id/imageView_cover"
android:layout_width="0dp"
android:layout_height="0dp"
@ -30,6 +30,7 @@
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintWidth_percent="0.3"
app:shapeAppearanceOverlay="@style/ShapeAppearanceOverlay.Kotatsu.Cover"
app:useExistingDrawable="true"
tools:background="@tools:sample/backgrounds/scenic[5]"
tools:ignore="ContentDescription,UnusedAttribute" />

@ -1,19 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.card.MaterialCardView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
style="@style/Widget.Material3.CardView.Outlined"
android:layout_width="wrap_content"
android:layout_height="@dimen/bookmark_item_height"
app:cardCornerRadius="12dp">
<org.koitharu.kotatsu.core.ui.widgets.CoverImageView
android:id="@+id/imageView_thumb"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:orientation="vertical"
android:scaleType="centerCrop"
tools:src="@drawable/ic_placeholder" />
</com.google.android.material.card.MaterialCardView>

@ -9,12 +9,11 @@
app:cardBackgroundColor="?attr/colorSurfaceContainerHighest"
tools:layout_width="140dp">
<org.koitharu.kotatsu.core.ui.widgets.CoverImageView
<org.koitharu.kotatsu.image.ui.CoverImageView
android:id="@+id/imageView_thumb"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:scaleType="centerCrop"
app:shapeAppearanceOverlay="@style/ShapeAppearanceOverlay.Kotatsu.Cover"
tools:ignore="ContentDescription"
tools:src="@tools:sample/backgrounds/scenic[5]" />

@ -6,61 +6,19 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/list_selector"
android:minHeight="98dp"
android:paddingVertical="4dp"
android:paddingStart="?android:listPreferredItemPaddingStart"
tools:ignore="RtlSymmetry">
<com.google.android.material.imageview.ShapeableImageView
android:id="@+id/imageView_cover3"
<org.koitharu.kotatsu.image.ui.CoverStackView
android:id="@+id/coversView"
android:layout_width="0dp"
android:layout_height="64dp"
android:layout_marginStart="24dp"
android:layout_marginBottom="12dp"
android:background="?attr/colorSecondaryContainer"
android:backgroundTintMode="src_atop"
android:scaleType="centerCrop"
android:layout_height="@dimen/category_covers_height"
app:coverSize="3.4dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintDimensionRatio="W,13:18"
app:layout_constraintDimensionRatio="13:18"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:shapeAppearanceOverlay="@style/ShapeAppearanceOverlay.Kotatsu.Cover.Small"
app:tintMode="src_atop"
tools:backgroundTint="#99FFFFFF"
tools:src="@tools:sample/backgrounds/scenic"
tools:tint="#99FFFFFF" />
<com.google.android.material.imageview.ShapeableImageView
android:id="@+id/imageView_cover2"
android:layout_width="0dp"
android:layout_height="64dp"
android:layout_marginStart="12dp"
android:background="?attr/colorSecondaryContainer"
android:backgroundTintMode="src_atop"
android:scaleType="centerCrop"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintDimensionRatio="W,13:18"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:shapeAppearanceOverlay="@style/ShapeAppearanceOverlay.Kotatsu.Cover.Small"
app:tintMode="src_atop"
tools:backgroundTint="#4DFFFFFF"
tools:src="@tools:sample/backgrounds/scenic"
tools:tint="#4DFFFFFF" />
<com.google.android.material.imageview.ShapeableImageView
android:id="@+id/imageView_cover1"
android:layout_width="0dp"
android:layout_height="64dp"
android:layout_marginTop="12dp"
android:background="?attr/colorSecondaryContainer"
android:backgroundTintMode="src_atop"
android:scaleType="centerCrop"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintDimensionRatio="W,13:18"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:shapeAppearanceOverlay="@style/ShapeAppearanceOverlay.Kotatsu.Cover.Small"
tools:src="@tools:sample/backgrounds/scenic" />
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/textView_title"
@ -74,7 +32,7 @@
android:textAppearance="?attr/textAppearanceBodyLarge"
app:layout_constraintBottom_toTopOf="@id/textView_subtitle"
app:layout_constraintEnd_toStartOf="@id/imageView_visible"
app:layout_constraintStart_toEndOf="@id/imageView_cover3"
app:layout_constraintStart_toEndOf="@id/coversView"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_chainStyle="packed" />
@ -91,7 +49,7 @@
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/imageView_visible"
app:layout_constraintHorizontal_bias="0"
app:layout_constraintStart_toEndOf="@id/imageView_cover3"
app:layout_constraintStart_toEndOf="@id/coversView"
app:layout_constraintTop_toBottomOf="@id/textView_title"
app:layout_constraintVertical_chainStyle="packed"
tools:text="@tools:sample/lorem[1]" />

@ -6,61 +6,19 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/list_selector"
android:minHeight="98dp"
android:paddingVertical="4dp"
android:paddingStart="?android:listPreferredItemPaddingStart"
tools:ignore="RtlSymmetry">
<com.google.android.material.imageview.ShapeableImageView
android:id="@+id/imageView_cover3"
<org.koitharu.kotatsu.image.ui.CoverStackView
android:id="@+id/coversView"
android:layout_width="0dp"
android:layout_height="64dp"
android:layout_marginStart="24dp"
android:layout_marginBottom="12dp"
android:background="?attr/colorSecondaryContainer"
android:backgroundTintMode="src_atop"
android:scaleType="centerCrop"
android:layout_height="@dimen/category_covers_height"
app:coverSize="3.4dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintDimensionRatio="W,13:18"
app:layout_constraintDimensionRatio="13:18"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:shapeAppearanceOverlay="@style/ShapeAppearanceOverlay.Kotatsu.Cover.Small"
app:tintMode="src_atop"
tools:backgroundTint="#99FFFFFF"
tools:src="@tools:sample/backgrounds/scenic"
tools:tint="#99FFFFFF" />
<com.google.android.material.imageview.ShapeableImageView
android:id="@+id/imageView_cover2"
android:layout_width="0dp"
android:layout_height="64dp"
android:layout_marginStart="12dp"
android:background="?attr/colorSecondaryContainer"
android:backgroundTintMode="src_atop"
android:scaleType="centerCrop"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintDimensionRatio="W,13:18"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:shapeAppearanceOverlay="@style/ShapeAppearanceOverlay.Kotatsu.Cover.Small"
app:tintMode="src_atop"
tools:backgroundTint="#4DFFFFFF"
tools:src="@tools:sample/backgrounds/scenic"
tools:tint="#4DFFFFFF" />
<com.google.android.material.imageview.ShapeableImageView
android:id="@+id/imageView_cover1"
android:layout_width="0dp"
android:layout_height="64dp"
android:layout_marginTop="12dp"
android:background="?attr/colorSecondaryContainer"
android:backgroundTintMode="src_atop"
android:scaleType="centerCrop"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintDimensionRatio="W,13:18"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:shapeAppearanceOverlay="@style/ShapeAppearanceOverlay.Kotatsu.Cover.Small"
tools:src="@tools:sample/backgrounds/scenic" />
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/textView_title"
@ -73,7 +31,7 @@
android:textAppearance="?attr/textAppearanceBodyLarge"
app:layout_constraintBottom_toTopOf="@id/textView_subtitle"
app:layout_constraintEnd_toStartOf="@id/imageView_edit"
app:layout_constraintStart_toEndOf="@id/imageView_cover3"
app:layout_constraintStart_toEndOf="@id/coversView"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_chainStyle="packed"
tools:text="@tools:sample/lorem[1]" />
@ -92,7 +50,7 @@
app:layout_constraintEnd_toStartOf="@id/imageView_tracker"
app:layout_constraintHorizontal_bias="0"
app:layout_constraintHorizontal_chainStyle="packed"
app:layout_constraintStart_toEndOf="@id/imageView_cover3"
app:layout_constraintStart_toEndOf="@id/coversView"
app:layout_constraintTop_toBottomOf="@id/textView_title"
app:layout_constraintVertical_chainStyle="packed"
tools:text="@tools:sample/lorem[1]" />

@ -13,13 +13,12 @@
android:layout_height="match_parent"
android:paddingBottom="12dp">
<com.google.android.material.imageview.ShapeableImageView
<org.koitharu.kotatsu.image.ui.CoverImageView
android:id="@+id/imageView_cover"
android:layout_width="64dp"
android:layout_height="64dp"
android:layout_marginStart="12dp"
android:layout_marginTop="12dp"
android:scaleType="centerCrop"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:shapeAppearanceOverlay="@style/ShapeAppearanceOverlay.Kotatsu.Cover.Medium"

@ -11,7 +11,7 @@
android:layout_width="match_parent"
android:layout_height="wrap_content">
<ImageView
<org.koitharu.kotatsu.core.image.CoilImageView
android:id="@+id/icon"
android:layout_width="120dp"
android:layout_height="120dp"

@ -7,7 +7,7 @@
android:layout_height="wrap_content"
android:padding="@dimen/margin_normal">
<ImageView
<org.koitharu.kotatsu.core.image.CoilImageView
android:id="@+id/icon"
android:layout_width="120dp"
android:layout_height="120dp"

@ -1,6 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
@ -9,12 +10,13 @@
android:paddingHorizontal="32dp"
android:paddingBottom="?actionBarSize">
<ImageView
<org.koitharu.kotatsu.core.image.CoilImageView
android:id="@+id/icon"
android:layout_width="192dp"
android:layout_height="192dp"
android:contentDescription="@null"
android:scaleType="fitCenter"
app:crossfadeEnabled="false"
tools:src="@drawable/ic_empty_favourites" />
<TextView

@ -12,7 +12,7 @@
android:padding="@dimen/list_spacing"
tools:layout_width="120dp">
<com.google.android.material.imageview.ShapeableImageView
<org.koitharu.kotatsu.core.ui.image.FaviconView
android:id="@+id/imageView_icon"
android:layout_width="72dp"
android:layout_height="72dp"
@ -20,10 +20,10 @@
android:labelFor="@id/textView_title"
android:padding="1dp"
android:scaleType="fitCenter"
app:iconStyle="@style/FaviconDrawable.Large"
app:shapeAppearance="?shapeAppearanceCornerMedium"
app:strokeColor="?colorOutline"
app:strokeWidth="1dp"
tools:src="@tools:sample/avatars" />
app:strokeWidth="1dp" />
<TextView
android:id="@+id/textView_title"

@ -11,7 +11,7 @@
android:gravity="center_vertical"
android:orientation="horizontal">
<com.google.android.material.imageview.ShapeableImageView
<org.koitharu.kotatsu.core.ui.image.FaviconView
android:id="@+id/imageView_icon"
android:layout_width="40dp"
android:layout_height="40dp"
@ -20,6 +20,7 @@
android:layout_marginBottom="8dp"
android:background="?colorSurfaceContainer"
android:scaleType="centerCrop"
app:iconStyle="@style/FaviconDrawable.Small"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"

@ -8,7 +8,7 @@
android:background="@drawable/list_selector"
android:clipChildren="false">
<com.google.android.material.imageview.ShapeableImageView
<org.koitharu.kotatsu.image.ui.CoverImageView
android:id="@+id/imageView_cover"
android:layout_width="40dp"
android:layout_height="40dp"

@ -11,12 +11,11 @@
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.google.android.material.imageview.ShapeableImageView
<org.koitharu.kotatsu.image.ui.CoverImageView
android:id="@+id/imageView_cover"
android:layout_width="98dp"
android:layout_height="0dp"
android:background="?colorSurfaceContainer"
android:scaleType="centerCrop"
app:layout_constraintDimensionRatio="13:18"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"

@ -16,13 +16,13 @@
android:layout_width="match_parent"
android:layout_height="wrap_content">
<org.koitharu.kotatsu.core.ui.widgets.CoverImageView
<org.koitharu.kotatsu.image.ui.CoverImageView
android:id="@+id/imageView_cover"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?colorSurfaceContainer"
android:orientation="horizontal"
android:scaleType="centerCrop"
app:allowRgb565="true"
app:shapeAppearanceOverlay="@style/ShapeAppearanceOverlay.Kotatsu.Cover"
tools:ignore="ContentDescription"
tools:src="@tools:sample/backgrounds/scenic[5]" />

@ -8,7 +8,7 @@
android:background="@drawable/list_selector"
android:clipChildren="false">
<com.google.android.material.imageview.ShapeableImageView
<org.koitharu.kotatsu.image.ui.CoverImageView
android:id="@+id/imageView_cover"
android:layout_width="40dp"
android:layout_height="40dp"
@ -16,11 +16,13 @@
android:layout_marginTop="8dp"
android:layout_marginBottom="8dp"
android:background="?colorSurfaceContainer"
android:scaleType="centerCrop"
app:aspectRationHeight="0"
app:aspectRationWidth="0"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:shapeAppearanceOverlay="@style/ShapeAppearanceOverlay.Kotatsu.Cover.Small"
app:trimImage="true"
tools:src="@tools:sample/backgrounds/scenic" />
<TextView

@ -11,17 +11,19 @@
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.google.android.material.imageview.ShapeableImageView
<org.koitharu.kotatsu.image.ui.CoverImageView
android:id="@+id/imageView_cover"
android:layout_width="0dp"
android:layout_height="118dp"
android:background="?colorSurfaceContainer"
android:scaleType="centerCrop"
app:aspectRationHeight="0"
app:aspectRationWidth="0"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintDimensionRatio="2:3"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:shapeAppearanceOverlay="@style/ShapeAppearanceOverlay.Kotatsu.Cover.Medium"
app:trimImage="true"
tools:src="@tools:sample/backgrounds/scenic" />
<org.koitharu.kotatsu.history.ui.util.ReadingProgressView

@ -8,11 +8,13 @@
android:layout_height="wrap_content"
app:cardBackgroundColor="?colorBackgroundFloating">
<org.koitharu.kotatsu.core.ui.widgets.CoverImageView
<org.koitharu.kotatsu.image.ui.CoverImageView
android:id="@+id/imageView_thumb"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:scaleType="centerCrop"
app:decodeRegion="true"
app:trimImage="true"
tools:src="@drawable/ic_placeholder" />
<TextView

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save