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

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

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

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

@ -1,8 +1,6 @@
package org.koitharu.kotatsu.bookmarks.ui.adapter package org.koitharu.kotatsu.bookmarks.ui.adapter
import android.content.Context import android.content.Context
import androidx.lifecycle.LifecycleOwner
import coil3.ImageLoader
import org.koitharu.kotatsu.bookmarks.domain.Bookmark import org.koitharu.kotatsu.bookmarks.domain.Bookmark
import org.koitharu.kotatsu.core.ui.BaseListAdapter import org.koitharu.kotatsu.core.ui.BaseListAdapter
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener 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 import org.koitharu.kotatsu.list.ui.model.ListModel
class BookmarksAdapter( class BookmarksAdapter(
coil: ImageLoader,
lifecycleOwner: LifecycleOwner,
clickListener: OnListItemClickListener<Bookmark>, clickListener: OnListItemClickListener<Bookmark>,
headerClickListener: ListHeaderClickListener?, headerClickListener: ListHeaderClickListener?,
) : BaseListAdapter<ListModel>(), FastScroller.SectionIndexer { ) : BaseListAdapter<ListModel>(), FastScroller.SectionIndexer {
init { init {
addDelegate(ListItemType.PAGE_THUMB, bookmarkLargeAD(coil, lifecycleOwner, clickListener)) addDelegate(ListItemType.PAGE_THUMB, bookmarkLargeAD(clickListener))
addDelegate(ListItemType.HEADER, listHeaderAD(headerClickListener)) addDelegate(ListItemType.HEADER, listHeaderAD(headerClickListener))
addDelegate(ListItemType.STATE_ERROR, errorStateListAD(null)) addDelegate(ListItemType.STATE_ERROR, errorStateListAD(null))
addDelegate(ListItemType.FOOTER_LOADING, loadingFooterAD()) addDelegate(ListItemType.FOOTER_LOADING, loadingFooterAD())
addDelegate(ListItemType.STATE_LOADING, loadingStateAD()) 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? { 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) 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) { override fun onTimeUpdate(animation: TimeAnimator?, totalTime: Long, deltaTime: Long) {
callback?.also { callback?.also {
updateColor() 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.annotation.StringRes
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.view.children import androidx.core.view.children
import androidx.lifecycle.findViewTreeLifecycleOwner
import coil3.ImageLoader import coil3.ImageLoader
import coil3.request.Disposable import coil3.request.Disposable
import coil3.request.ImageRequest import coil3.request.ImageRequest
@ -15,6 +16,7 @@ import coil3.request.allowRgb565
import coil3.request.crossfade import coil3.request.crossfade
import coil3.request.error import coil3.request.error
import coil3.request.fallback import coil3.request.fallback
import coil3.request.lifecycle
import coil3.request.placeholder import coil3.request.placeholder
import coil3.request.transformations import coil3.request.transformations
import coil3.transform.RoundedCornersTransformation import coil3.transform.RoundedCornersTransformation
@ -203,6 +205,7 @@ class ChipsView @JvmOverloads constructor(
.target(ChipIconTarget(this)) .target(ChipIconTarget(this))
.placeholder(placeholder) .placeholder(placeholder)
.fallback(placeholder) .fallback(placeholder)
.lifecycle(this@ChipsView.findViewTreeLifecycleOwner())
.error(placeholder) .error(placeholder)
.transformations(RoundedCornersTransformation(resources.getDimension(R.dimen.chip_icon_corner))) .transformations(RoundedCornersTransformation(resources.getDimension(R.dimen.chip_icon_corner)))
.allowRgb565(true) .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 package org.koitharu.kotatsu.core.util.ext
import android.content.Context
import android.graphics.drawable.Drawable 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.Extras
import coil3.ImageLoader import coil3.ImageLoader
import coil3.asDrawable import coil3.asDrawable
@ -16,48 +11,11 @@ import coil3.request.ImageResult
import coil3.request.Options import coil3.request.Options
import coil3.request.SuccessResult import coil3.request.SuccessResult
import coil3.request.bitmapConfig 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.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.bookmarks.domain.Bookmark
import org.koitharu.kotatsu.core.image.RegionBitmapDecoder 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.Manga
import org.koitharu.kotatsu.parsers.model.MangaSource 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()) fun ImageRequest.Builder.enqueueWith(loader: ImageLoader) = loader.enqueue(build())
@ -79,10 +37,6 @@ fun ImageResult.toBitmapOrNull() = when (this) {
is ErrorResult -> null is ErrorResult -> null
} }
fun ImageRequest.Builder.indicator(indicators: List<BaseProgressIndicator<*>>): ImageRequest.Builder {
return addListener(ImageRequestIndicatorListener(indicators))
}
fun ImageRequest.Builder.decodeRegion( fun ImageRequest.Builder.decodeRegion(
scroll: Int = RegionBitmapDecoder.SCROLL_UNDEFINED, scroll: Int = RegionBitmapDecoder.SCROLL_UNDEFINED,
): ImageRequest.Builder = apply { ): ImageRequest.Builder = apply {
@ -90,19 +44,13 @@ fun ImageRequest.Builder.decodeRegion(
extras[RegionBitmapDecoder.regionScrollKey] = scroll 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 { fun ImageRequest.Builder.mangaSourceExtra(source: MangaSource?): ImageRequest.Builder = apply {
extras[mangaSourceKey] = source extras[mangaSourceKey] = source
} }
fun ImageRequest.Builder.mangaExtra(manga: Manga): ImageRequest.Builder = apply { fun ImageRequest.Builder.mangaExtra(manga: Manga?): ImageRequest.Builder = apply {
extras[mangaKey] = manga extras[mangaKey] = manga
mangaSourceExtra(manga.source) mangaSourceExtra(manga?.source)
} }
fun ImageRequest.Builder.bookmarkExtra(bookmark: Bookmark): ImageRequest.Builder = apply { 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) 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? { suspend fun ImageLoader.fetch(data: Any, options: Options): FetchResult? {
val mappedData = components.map(data, options) val mappedData = components.map(data, options)
val fetcher = components.newFetcher(mappedData, options, this)?.first val fetcher = components.newFetcher(mappedData, options, this)?.first
return fetcher?.fetch() 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 mangaKey = Extras.Key<Manga?>(null)
val bookmarkKey = Extras.Key<Bookmark?>(null) val bookmarkKey = Extras.Key<Bookmark?>(null)
val mangaSourceKey = Extras.Key<MangaSource?>(null) val mangaSourceKey = Extras.Key<MangaSource?>(null)

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

@ -22,19 +22,12 @@ import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import androidx.transition.TransitionManager import androidx.transition.TransitionManager
import coil3.ImageLoader import coil3.ImageLoader
import coil3.request.ImageRequest import coil3.request.ImageRequest
import coil3.request.SuccessResult
import coil3.request.allowRgb565 import coil3.request.allowRgb565
import coil3.request.crossfade import coil3.request.crossfade
import coil3.request.error
import coil3.request.fallback
import coil3.request.lifecycle import coil3.request.lifecycle
import coil3.request.placeholder
import coil3.request.target
import coil3.request.transformations import coil3.request.transformations
import coil3.size.Precision import coil3.size.Precision
import coil3.size.Scale
import coil3.transform.RoundedCornersTransformation import coil3.transform.RoundedCornersTransformation
import coil3.util.CoilUtils
import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.chip.Chip import com.google.android.material.chip.Chip
import dagger.hilt.android.AndroidEntryPoint 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.BaseActivity
import org.koitharu.kotatsu.core.ui.BaseListAdapter import org.koitharu.kotatsu.core.ui.BaseListAdapter
import org.koitharu.kotatsu.core.ui.dialog.buildAlertDialog 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.FaviconDrawable
import org.koitharu.kotatsu.core.ui.image.TextDrawable import org.koitharu.kotatsu.core.ui.image.TextDrawable
import org.koitharu.kotatsu.core.ui.image.TextViewTarget 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.LocaleUtils
import org.koitharu.kotatsu.core.util.ext.consume import org.koitharu.kotatsu.core.util.ext.consume
import org.koitharu.kotatsu.core.util.ext.copyToClipboard 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.drawableStart
import org.koitharu.kotatsu.core.util.ext.end import org.koitharu.kotatsu.core.util.ext.end
import org.koitharu.kotatsu.core.util.ext.enqueueWith import org.koitharu.kotatsu.core.util.ext.enqueueWith
@ -369,8 +358,7 @@ class DetailsActivity :
.addDelegate( .addDelegate(
ListItemType.MANGA_GRID, ListItemType.MANGA_GRID,
mangaGridItemAD( mangaGridItemAD(
coil, this, sizeResolver = StaticItemSizeResolver(resources.getDimensionPixelSize(R.dimen.smaller_grid_width)),
StaticItemSizeResolver(resources.getDimensionPixelSize(R.dimen.smaller_grid_width)),
) { item, view -> ) { item, view ->
router.openDetails(item) router.openDetails(item)
}, },
@ -505,28 +493,7 @@ class DetailsActivity :
} }
private fun loadCover(imageUrl: String?) { private fun loadCover(imageUrl: String?) {
viewBinding.imageViewCover.isEnabled = !imageUrl.isNullOrEmpty() viewBinding.imageViewCover.setImageAsync(imageUrl, viewModel.getMangaOrNull())
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)
} }
private fun String.withEstimatedTime(time: ReadingTime?): String { private fun String.withEstimatedTime(time: ReadingTime?): String {

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

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

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

@ -1,20 +1,13 @@
package org.koitharu.kotatsu.details.ui.scrobbling package org.koitharu.kotatsu.details.ui.scrobbling
import androidx.lifecycle.LifecycleOwner
import coil3.ImageLoader
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.nav.AppRouter 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.databinding.ItemScrobblingInfoBinding
import org.koitharu.kotatsu.list.ui.model.ListModel import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblingInfo import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblingInfo
fun scrobblingInfoAD( fun scrobblingInfoAD(
lifecycleOwner: LifecycleOwner,
coil: ImageLoader,
router: AppRouter, router: AppRouter,
) = adapterDelegateViewBinding<ScrobblingInfo, ListModel, ItemScrobblingInfoBinding>( ) = adapterDelegateViewBinding<ScrobblingInfo, ListModel, ItemScrobblingInfoBinding>(
{ layoutInflater, parent -> ItemScrobblingInfoBinding.inflate(layoutInflater, parent, false) }, { layoutInflater, parent -> ItemScrobblingInfoBinding.inflate(layoutInflater, parent, false) },
@ -24,10 +17,7 @@ fun scrobblingInfoAD(
} }
bind { bind {
binding.imageViewCover.newImageRequest(lifecycleOwner, item.coverUrl)?.run { binding.imageViewCover.setImageAsync(item.coverUrl)
defaultPlaceholders(context)
enqueueWith(coil)
}
binding.textViewTitle.setText(item.scrobbler.titleResId) binding.textViewTitle.setText(item.scrobbler.titleResId)
binding.imageViewIcon.setImageResource(item.scrobbler.iconResId) binding.imageViewIcon.setImageResource(item.scrobbler.iconResId)
binding.ratingBar.rating = item.rating * binding.ratingBar.numStars 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.nav.router
import org.koitharu.kotatsu.core.ui.sheet.BaseAdaptiveSheet import org.koitharu.kotatsu.core.ui.sheet.BaseAdaptiveSheet
import org.koitharu.kotatsu.core.util.ext.consume 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.getDisplayMessage
import org.koitharu.kotatsu.core.util.ext.newImageRequest
import org.koitharu.kotatsu.core.util.ext.observe import org.koitharu.kotatsu.core.util.ext.observe
import org.koitharu.kotatsu.core.util.ext.observeEvent import org.koitharu.kotatsu.core.util.ext.observeEvent
import org.koitharu.kotatsu.core.util.ext.sanitize import org.koitharu.kotatsu.core.util.ext.sanitize
@ -137,10 +134,7 @@ class ScrobblingInfoSheet :
binding.spinnerStatus.setSelection(scrobbling.status?.ordinal ?: -1) binding.spinnerStatus.setSelection(scrobbling.status?.ordinal ?: -1)
binding.imageViewLogo.contentDescription = getString(scrobbling.scrobbler.titleResId) binding.imageViewLogo.contentDescription = getString(scrobbling.scrobbler.titleResId)
binding.imageViewLogo.setImageResource(scrobbling.scrobbler.iconResId) binding.imageViewLogo.setImageResource(scrobbling.scrobbler.iconResId)
binding.imageViewCover.newImageRequest(viewLifecycleOwner, scrobbling.coverUrl)?.apply { binding.imageViewCover.setImageAsync(scrobbling.coverUrl)
defaultPlaceholders(binding.imageViewCover.context)
enqueueWith(coil)
}
} }
override fun onMenuItemClick(item: MenuItem): Boolean { override fun onMenuItemClick(item: MenuItem): Boolean {

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

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

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

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

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

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

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

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

@ -1,33 +1,16 @@
package org.koitharu.kotatsu.favourites.ui.categories.adapter package org.koitharu.kotatsu.favourites.ui.categories.adapter
import android.annotation.SuppressLint 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.MotionEvent
import android.view.View import android.view.View
import android.view.View.OnClickListener import android.view.View.OnClickListener
import android.view.View.OnLongClickListener import android.view.View.OnLongClickListener
import android.view.View.OnTouchListener import android.view.View.OnTouchListener
import androidx.core.graphics.ColorUtils
import androidx.core.view.isGone import androidx.core.view.isGone
import androidx.core.view.isVisible 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 com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
import org.koitharu.kotatsu.R 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.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.ItemCategoriesAllBinding
import org.koitharu.kotatsu.databinding.ItemCategoryBinding import org.koitharu.kotatsu.databinding.ItemCategoryBinding
import org.koitharu.kotatsu.favourites.ui.categories.FavouriteCategoriesListListener import org.koitharu.kotatsu.favourites.ui.categories.FavouriteCategoriesListListener
@ -35,8 +18,6 @@ import org.koitharu.kotatsu.list.ui.model.ListModel
@SuppressLint("ClickableViewAccessibility") @SuppressLint("ClickableViewAccessibility")
fun categoryAD( fun categoryAD(
coil: ImageLoader,
lifecycleOwner: LifecycleOwner,
clickListener: FavouriteCategoriesListListener, clickListener: FavouriteCategoriesListListener,
) = adapterDelegateViewBinding<CategoryListModel, ListModel, ItemCategoryBinding>( ) = adapterDelegateViewBinding<CategoryListModel, ListModel, ItemCategoryBinding>(
{ inflater, parent -> ItemCategoryBinding.inflate(inflater, parent, false) }, { 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 && override fun onTouch(v: View?, event: MotionEvent): Boolean = event.actionMasked == MotionEvent.ACTION_DOWN &&
clickListener.onDragHandleTouch(this@adapterDelegateViewBinding) 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.setOnClickListener(eventListener)
itemView.setOnLongClickListener(eventListener) itemView.setOnLongClickListener(eventListener)
binding.imageViewEdit.setOnClickListener(eventListener) binding.imageViewEdit.setOnClickListener(eventListener)
@ -88,24 +53,11 @@ fun categoryAD(
} }
binding.imageViewTracker.isVisible = item.category.isTrackingEnabled binding.imageViewTracker.isVisible = item.category.isTrackingEnabled
binding.imageViewHidden.isGone = item.category.isVisibleInLibrary binding.imageViewHidden.isGone = item.category.isVisibleInLibrary
repeat(coverViews.size) { i -> binding.coversView.setCoversAsync(item.covers)
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)
}
}
} }
} }
fun allCategoriesAD( fun allCategoriesAD(
coil: ImageLoader,
lifecycleOwner: LifecycleOwner,
clickListener: FavouriteCategoriesListListener, clickListener: FavouriteCategoriesListListener,
) = adapterDelegateViewBinding<AllCategoriesListModel, ListModel, ItemCategoriesAllBinding>( ) = adapterDelegateViewBinding<AllCategoriesListModel, ListModel, ItemCategoriesAllBinding>(
{ inflater, parent -> ItemCategoriesAllBinding.inflate(inflater, parent, false) }, { inflater, parent -> ItemCategoriesAllBinding.inflate(inflater, parent, false) },
@ -117,22 +69,7 @@ fun allCategoriesAD(
clickListener.onItemClick(null, v) 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) itemView.setOnClickListener(eventListener)
binding.imageViewVisible.setOnClickListener(eventListener) binding.imageViewVisible.setOnClickListener(eventListener)
@ -154,17 +91,6 @@ fun allCategoriesAD(
R.drawable.ic_eye_off R.drawable.ic_eye_off
}, },
) )
repeat(coverViews.size) { i -> binding.coversView.setCoversAsync(item.covers)
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)
}
}
} }
} }

@ -1,24 +1,12 @@
package org.koitharu.kotatsu.favourites.ui.categories.select package org.koitharu.kotatsu.favourites.ui.categories.select
import android.content.DialogInterface import android.content.DialogInterface
import android.content.res.ColorStateList
import android.graphics.Color
import android.graphics.drawable.ColorDrawable
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.Toast 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 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.checkbox.MaterialCheckBox
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import dagger.hilt.android.AndroidEntryPoint 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.nav.router
import org.koitharu.kotatsu.core.ui.AlertDialogFragment import org.koitharu.kotatsu.core.ui.AlertDialogFragment
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener 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.getDisplayMessage
import org.koitharu.kotatsu.core.util.ext.getThemeColor
import org.koitharu.kotatsu.core.util.ext.joinToStringWithLimit 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.observe
import org.koitharu.kotatsu.core.util.ext.observeEvent import org.koitharu.kotatsu.core.util.ext.observeEvent
import org.koitharu.kotatsu.databinding.SheetFavoriteCategoriesBinding import org.koitharu.kotatsu.databinding.SheetFavoriteCategoriesBinding
import org.koitharu.kotatsu.favourites.ui.categories.select.adapter.MangaCategoriesAdapter import org.koitharu.kotatsu.favourites.ui.categories.select.adapter.MangaCategoriesAdapter
import org.koitharu.kotatsu.favourites.ui.categories.select.model.MangaCategoryItem import org.koitharu.kotatsu.favourites.ui.categories.select.model.MangaCategoryItem
import javax.inject.Inject
@AndroidEntryPoint @AndroidEntryPoint
class FavoriteDialog : AlertDialogFragment<SheetFavoriteCategoriesBinding>(), class FavoriteDialog : AlertDialogFragment<SheetFavoriteCategoriesBinding>(),
@ -47,9 +28,6 @@ class FavoriteDialog : AlertDialogFragment<SheetFavoriteCategoriesBinding>(),
private val viewModel by viewModels<FavoriteDialogViewModel>() private val viewModel by viewModels<FavoriteDialogViewModel>()
@Inject
lateinit var coil: ImageLoader
override fun onCreateViewBinding( override fun onCreateViewBinding(
inflater: LayoutInflater, inflater: LayoutInflater,
container: ViewGroup?, container: ViewGroup?,
@ -66,7 +44,7 @@ class FavoriteDialog : AlertDialogFragment<SheetFavoriteCategoriesBinding>(),
savedInstanceState: Bundle?, savedInstanceState: Bundle?,
) { ) {
super.onViewBindingCreated(binding, savedInstanceState) super.onViewBindingCreated(binding, savedInstanceState)
val adapter = MangaCategoriesAdapter(coil, viewLifecycleOwner, this) val adapter = MangaCategoriesAdapter(this)
binding.recyclerViewCategories.adapter = adapter binding.recyclerViewCategories.adapter = adapter
viewModel.content.observe(viewLifecycleOwner, adapter) viewModel.content.observe(viewLifecycleOwner, adapter)
viewModel.onError.observeEvent(viewLifecycleOwner, ::onError) viewModel.onError.observeEvent(viewLifecycleOwner, ::onError)
@ -88,42 +66,7 @@ class FavoriteDialog : AlertDialogFragment<SheetFavoriteCategoriesBinding>(),
private fun bindHeader() { private fun bindHeader() {
val manga = viewModel.manga val manga = viewModel.manga
val binding = viewBinding ?: return 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 } binding.textViewTitle.text = manga.joinToStringWithLimit(binding.root.context, 92) { it.title }
binding.coversStack.setCoversAsync(manga)
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)
}
}
}
} }
} }

@ -1,7 +1,5 @@
package org.koitharu.kotatsu.favourites.ui.categories.select.adapter 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.BaseListAdapter
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.favourites.ui.categories.select.model.MangaCategoryItem 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 import org.koitharu.kotatsu.list.ui.model.ListModel
class MangaCategoriesAdapter( class MangaCategoriesAdapter(
coil: ImageLoader,
lifecycleOwner: LifecycleOwner,
clickListener: OnListItemClickListener<MangaCategoryItem>, clickListener: OnListItemClickListener<MangaCategoryItem>,
) : BaseListAdapter<ListModel>() { ) : BaseListAdapter<ListModel>() {
init { init {
addDelegate(ListItemType.NAV_ITEM, mangaCategoryAD(clickListener)) addDelegate(ListItemType.NAV_ITEM, mangaCategoryAD(clickListener))
addDelegate(ListItemType.STATE_LOADING, loadingStateAD()) 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.Fragment
import androidx.fragment.app.viewModels import androidx.fragment.app.viewModels
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import coil3.ImageLoader
import com.google.android.material.tabs.TabLayoutMediator import com.google.android.material.tabs.TabLayoutMediator
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import org.koitharu.kotatsu.R 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.RecyclerViewOwner
import org.koitharu.kotatsu.core.ui.util.ReversibleActionObserver import org.koitharu.kotatsu.core.ui.util.ReversibleActionObserver
import org.koitharu.kotatsu.core.util.ext.addMenuProvider 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.findCurrentPagerFragment
import org.koitharu.kotatsu.core.util.ext.newImageRequest
import org.koitharu.kotatsu.core.util.ext.observe import org.koitharu.kotatsu.core.util.ext.observe
import org.koitharu.kotatsu.core.util.ext.observeEvent import org.koitharu.kotatsu.core.util.ext.observeEvent
import org.koitharu.kotatsu.core.util.ext.recyclerView 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.core.util.ext.setTextAndVisible
import org.koitharu.kotatsu.databinding.FragmentFavouritesContainerBinding import org.koitharu.kotatsu.databinding.FragmentFavouritesContainerBinding
import org.koitharu.kotatsu.databinding.ItemEmptyStateBinding import org.koitharu.kotatsu.databinding.ItemEmptyStateBinding
import javax.inject.Inject
@AndroidEntryPoint @AndroidEntryPoint
class FavouritesContainerFragment : BaseFragment<FragmentFavouritesContainerBinding>(), class FavouritesContainerFragment : BaseFragment<FragmentFavouritesContainerBinding>(),
@ -41,9 +37,6 @@ class FavouritesContainerFragment : BaseFragment<FragmentFavouritesContainerBind
ViewStub.OnInflateListener, ViewStub.OnInflateListener,
View.OnClickListener { View.OnClickListener {
@Inject
lateinit var coil: ImageLoader
private val viewModel: FavouritesContainerViewModel by viewModels() private val viewModel: FavouritesContainerViewModel by viewModels()
override val recyclerView: RecyclerView? override val recyclerView: RecyclerView?
@ -96,7 +89,7 @@ class FavouritesContainerFragment : BaseFragment<FragmentFavouritesContainerBind
override fun onInflate(stub: ViewStub?, inflated: View) { override fun onInflate(stub: ViewStub?, inflated: View) {
val stubBinding = ItemEmptyStateBinding.bind(inflated) 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.textPrimary.setText(R.string.text_empty_holder_primary)
stubBinding.textSecondary.setTextAndVisible(R.string.empty_favourite_categories) stubBinding.textSecondary.setTextAndVisible(R.string.empty_favourite_categories)
stubBinding.buttonRetry.setTextAndVisible(R.string.manage) stubBinding.buttonRetry.setTextAndVisible(R.string.manage)

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

@ -73,9 +73,7 @@ class HistoryListFragment : MangaListFragment() {
} }
override fun onCreateAdapter() = HistoryListAdapter( override fun onCreateAdapter() = HistoryListAdapter(
coil,
viewLifecycleOwner,
this, 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 { protected open fun onCreateAdapter(): MangaListAdapter {
return MangaListAdapter( return MangaListAdapter(
coil = coil,
lifecycleOwner = viewLifecycleOwner,
listener = this, 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 package org.koitharu.kotatsu.list.ui.adapter
import androidx.lifecycle.LifecycleOwner
import coil3.ImageLoader
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding 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.core.util.ext.setTextAndVisible
import org.koitharu.kotatsu.databinding.ItemEmptyCardBinding import org.koitharu.kotatsu.databinding.ItemEmptyCardBinding
import org.koitharu.kotatsu.list.ui.model.EmptyHint import org.koitharu.kotatsu.list.ui.model.EmptyHint
import org.koitharu.kotatsu.list.ui.model.ListModel import org.koitharu.kotatsu.list.ui.model.ListModel
fun emptyHintAD( fun emptyHintAD(
coil: ImageLoader,
lifecycleOwner: LifecycleOwner,
listener: ListStateHolderListener, listener: ListStateHolderListener,
) = adapterDelegateViewBinding<EmptyHint, ListModel, ItemEmptyCardBinding>( ) = adapterDelegateViewBinding<EmptyHint, ListModel, ItemEmptyCardBinding>(
{ inflater, parent -> ItemEmptyCardBinding.inflate(inflater, parent, false) }, { inflater, parent -> ItemEmptyCardBinding.inflate(inflater, parent, false) },
@ -21,7 +15,7 @@ fun emptyHintAD(
binding.buttonRetry.setOnClickListener { listener.onEmptyActionClick() } binding.buttonRetry.setOnClickListener { listener.onEmptyActionClick() }
bind { bind {
binding.icon.newImageRequest(lifecycleOwner, item.icon)?.enqueueWith(coil) binding.icon.setImageAsync(item.icon)
binding.textPrimary.setText(item.textPrimary) binding.textPrimary.setText(item.textPrimary)
binding.textSecondary.setTextAndVisible(item.textSecondary) binding.textSecondary.setTextAndVisible(item.textSecondary)
binding.buttonRetry.setTextAndVisible(item.actionStringRes) binding.buttonRetry.setTextAndVisible(item.actionStringRes)

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

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

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

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

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

@ -10,26 +10,12 @@ import androidx.core.view.WindowInsetsCompat
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.fragment.app.viewModels import androidx.fragment.app.viewModels
import coil3.ImageLoader 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 com.google.android.material.chip.Chip
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.nav.router import org.koitharu.kotatsu.core.nav.router
import org.koitharu.kotatsu.core.ui.BaseFragment 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.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.observe
import org.koitharu.kotatsu.core.util.ext.textAndVisible import org.koitharu.kotatsu.core.util.ext.textAndVisible
import org.koitharu.kotatsu.databinding.FragmentPreviewBinding import org.koitharu.kotatsu.databinding.FragmentPreviewBinding
@ -143,27 +129,7 @@ class PreviewFragment : BaseFragment<FragmentPreviewBinding>(), View.OnClickList
private fun loadCover(manga: Manga) { private fun loadCover(manga: Manga) {
val imageUrl = manga.largeCoverUrl.ifNullOrEmpty { manga.coverUrl } val imageUrl = manga.largeCoverUrl.ifNullOrEmpty { manga.coverUrl }
val lastResult = CoilUtils.result(requireViewBinding().imageViewCover) requireViewBinding().imageViewCover.setImageAsync(imageUrl, manga)
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)
} }
private fun onTagsChipsChanged(chips: List<ChipsView.ChipModel>) { private fun onTagsChipsChanged(chips: List<ChipsView.ChipModel>) {

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

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

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

@ -1,18 +1,17 @@
package org.koitharu.kotatsu.reader.ui.colorfilter package org.koitharu.kotatsu.reader.ui.colorfilter
import android.content.res.Resources import android.content.res.Resources
import android.graphics.Bitmap
import android.os.Bundle import android.os.Bundle
import android.view.View import android.view.View
import android.widget.CompoundButton import android.widget.CompoundButton
import android.widget.ImageView
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.core.view.WindowInsetsCompat import androidx.core.view.WindowInsetsCompat
import coil3.ImageLoader import coil3.ImageLoader
import coil3.asDrawable
import coil3.request.ErrorResult
import coil3.request.ImageRequest import coil3.request.ImageRequest
import coil3.request.bitmapConfig import coil3.request.SuccessResult
import coil3.request.error
import coil3.size.Scale
import coil3.size.ViewSizeResolver
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.slider.LabelFormatter import com.google.android.material.slider.LabelFormatter
import com.google.android.material.slider.Slider 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.R
import org.koitharu.kotatsu.core.ui.BaseActivity import org.koitharu.kotatsu.core.ui.BaseActivity
import org.koitharu.kotatsu.core.util.ext.consumeAllSystemBarsInsets 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.observe
import org.koitharu.kotatsu.core.util.ext.observeEvent import org.koitharu.kotatsu.core.util.ext.observeEvent
import org.koitharu.kotatsu.core.util.ext.setChecked import org.koitharu.kotatsu.core.util.ext.setChecked
import org.koitharu.kotatsu.core.util.ext.setValueRounded import org.koitharu.kotatsu.core.util.ext.setValueRounded
import org.koitharu.kotatsu.core.util.ext.systemBarsInsets 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.databinding.ActivityColorFilterBinding
import org.koitharu.kotatsu.parsers.model.MangaPage import org.koitharu.kotatsu.parsers.model.MangaPage
import org.koitharu.kotatsu.parsers.util.format import org.koitharu.kotatsu.parsers.util.format
import org.koitharu.kotatsu.parsers.util.nullIfEmpty
import org.koitharu.kotatsu.reader.domain.ReaderColorFilter import org.koitharu.kotatsu.reader.domain.ReaderColorFilter
import javax.inject.Inject import javax.inject.Inject
@ -50,7 +45,7 @@ class ColorFilterConfigActivity :
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(ActivityColorFilterBinding.inflate(layoutInflater)) setContentView(ActivityColorFilterBinding.inflate(layoutInflater))
setDisplayHomeAsUp(true, true) setDisplayHomeAsUp(isEnabled = true, showUpAsClose = true)
viewBinding.sliderBrightness.addOnChangeListener(this) viewBinding.sliderBrightness.addOnChangeListener(this)
viewBinding.sliderContrast.addOnChangeListener(this) viewBinding.sliderContrast.addOnChangeListener(this)
val formatter = PercentLabelFormatter(resources) val formatter = PercentLabelFormatter(resources)
@ -128,19 +123,17 @@ class ColorFilterConfigActivity :
viewBinding.imageViewAfter.colorFilter = readerColorFilter?.toColorFilter() viewBinding.imageViewAfter.colorFilter = readerColorFilter?.toColorFilter()
} }
private fun loadPreview(page: MangaPage) { private fun loadPreview(page: MangaPage) = with(viewBinding.imageViewBefore) {
val data: Any = page.preview?.nullIfEmpty() ?: page addImageRequestListener(
ImageRequest.Builder(this@ColorFilterConfigActivity) ImageRequestIndicatorListener(
.data(data) listOf(
.scale(Scale.FILL) viewBinding.progressBefore,
.decodeRegion() viewBinding.progressAfter,
.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) addImageRequestListener(ShadowImageListener(viewBinding.imageViewAfter))
.size(ViewSizeResolver(viewBinding.imageViewBefore)) setImageAsync(page)
.target(DoubleViewTarget(viewBinding.imageViewBefore, viewBinding.imageViewAfter))
.enqueueWith(coil)
} }
private fun onLoadingChanged(isLoading: Boolean) { private fun onLoadingChanged(isLoading: Boolean) {
@ -160,4 +153,24 @@ class ColorFilterConfigActivity :
return pattern.format(percent) 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.WindowInsetsCompat
import androidx.core.view.updatePadding import androidx.core.view.updatePadding
import coil3.ImageLoader import coil3.ImageLoader
import coil3.request.error
import coil3.request.fallback
import coil3.request.placeholder
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import org.koitharu.kotatsu.R 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.BaseActivity
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.util.ext.consumeAllSystemBarsInsets 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.observe
import org.koitharu.kotatsu.core.util.ext.observeEvent import org.koitharu.kotatsu.core.util.ext.observeEvent
import org.koitharu.kotatsu.core.util.ext.showOrHide import org.koitharu.kotatsu.core.util.ext.showOrHide
@ -113,15 +107,11 @@ class ScrobblerConfigActivity : BaseActivity<ActivityScrobblerConfigBinding>(),
private fun onUserChanged(user: ScrobblerUser?) { private fun onUserChanged(user: ScrobblerUser?) {
if (user == null) { if (user == null) {
viewBinding.imageViewAvatar.disposeImageRequest() viewBinding.imageViewAvatar.disposeImage()
viewBinding.imageViewAvatar.setImageResource(appcompatR.drawable.abc_ic_menu_overflow_material) viewBinding.imageViewAvatar.setImageResource(appcompatR.drawable.abc_ic_menu_overflow_material)
return return
} }
viewBinding.imageViewAvatar.newImageRequest(this, user.avatar) viewBinding.imageViewAvatar.setImageAsync(user.avatar)
?.placeholder(R.drawable.bg_badge_empty)
?.fallback(R.drawable.ic_shortcut_default)
?.error(R.drawable.ic_shortcut_default)
?.enqueueWith(coil)
} }
private fun onLoadingStateChanged(isLoading: Boolean) { private fun onLoadingStateChanged(isLoading: Boolean) {

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

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

@ -20,7 +20,7 @@ class ScrobblerSelectorAdapter(
init { init {
addDelegate(ListItemType.STATE_LOADING, loadingStateAD()) 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.FOOTER_LOADING, loadingFooterAD())
addDelegate(ListItemType.HINT_EMPTY, scrobblerHintAD(stateHolderListener)) addDelegate(ListItemType.HINT_EMPTY, scrobblerHintAD(stateHolderListener))
} }

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

@ -76,7 +76,7 @@ class SearchActivity :
router.openList(item.source, item.listFilter, item.sortOrder) 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) val selectionDecoration = MangaSelectionDecoration(this)
selectionController = ListSelectionController( selectionController = ListSelectionController(
appCompatDelegate = delegate, appCompatDelegate = delegate,

@ -34,8 +34,6 @@ class SearchAdapter(
ListItemType.MANGA_NESTED_GROUP, ListItemType.MANGA_NESTED_GROUP,
searchResultsAD( searchResultsAD(
sharedPool = pool, sharedPool = pool,
lifecycleOwner = lifecycleOwner,
coil = coil,
sizeResolver = sizeResolver, sizeResolver = sizeResolver,
selectionDecoration = selectionDecoration, selectionDecoration = selectionDecoration,
listener = listener, listener = listener,
@ -44,7 +42,7 @@ class SearchAdapter(
) )
addDelegate(ListItemType.STATE_LOADING, loadingStateAD()) addDelegate(ListItemType.STATE_LOADING, loadingStateAD())
addDelegate(ListItemType.FOOTER_LOADING, loadingFooterAD()) 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.STATE_ERROR, errorStateListAD(listener))
addDelegate(ListItemType.FOOTER_BUTTON, buttonFooterAD(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.isGone
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.lifecycle.LifecycleOwner
import androidx.recyclerview.widget.RecyclerView.RecycledViewPool import androidx.recyclerview.widget.RecyclerView.RecycledViewPool
import coil3.ImageLoader
import com.hannesdorfmann.adapterdelegates4.ListDelegationAdapter import com.hannesdorfmann.adapterdelegates4.ListDelegationAdapter
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
@ -24,8 +22,6 @@ import org.koitharu.kotatsu.search.ui.multi.SearchResultsListModel
fun searchResultsAD( fun searchResultsAD(
sharedPool: RecycledViewPool, sharedPool: RecycledViewPool,
lifecycleOwner: LifecycleOwner,
coil: ImageLoader,
sizeResolver: ItemSizeResolver, sizeResolver: ItemSizeResolver,
selectionDecoration: MangaSelectionDecoration, selectionDecoration: MangaSelectionDecoration,
listener: OnListItemClickListener<Manga>, listener: OnListItemClickListener<Manga>,
@ -35,9 +31,7 @@ fun searchResultsAD(
) { ) {
binding.recyclerView.setRecycledViewPool(sharedPool) binding.recyclerView.setRecycledViewPool(sharedPool)
val adapter = ListDelegationAdapter( val adapter = ListDelegationAdapter(mangaGridItemAD(sizeResolver, listener))
mangaGridItemAD(coil, lifecycleOwner, sizeResolver, listener),
)
binding.recyclerView.addItemDecoration(selectionDecoration) binding.recyclerView.addItemDecoration(selectionDecoration)
binding.recyclerView.adapter = adapter binding.recyclerView.adapter = adapter
val spacing = context.resources.getDimensionPixelOffset(R.dimen.grid_spacing_outer) val spacing = context.resources.getDimensionPixelOffset(R.dimen.grid_spacing_outer)

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

@ -1,27 +1,13 @@
package org.koitharu.kotatsu.search.ui.suggestion.adapter 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 com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.model.getSummary import org.koitharu.kotatsu.core.model.getSummary
import org.koitharu.kotatsu.core.model.getTitle 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.databinding.ItemSearchSuggestionSourceBinding
import org.koitharu.kotatsu.search.ui.suggestion.SearchSuggestionListener import org.koitharu.kotatsu.search.ui.suggestion.SearchSuggestionListener
import org.koitharu.kotatsu.search.ui.suggestion.model.SearchSuggestionItem import org.koitharu.kotatsu.search.ui.suggestion.model.SearchSuggestionItem
fun searchSuggestionSourceAD( fun searchSuggestionSourceAD(
coil: ImageLoader,
lifecycleOwner: LifecycleOwner,
listener: SearchSuggestionListener, listener: SearchSuggestionListener,
) = adapterDelegateViewBinding<SearchSuggestionItem.Source, SearchSuggestionItem, ItemSearchSuggestionSourceBinding>( ) = adapterDelegateViewBinding<SearchSuggestionItem.Source, SearchSuggestionItem, ItemSearchSuggestionSourceBinding>(
{ inflater, parent -> ItemSearchSuggestionSourceBinding.inflate(inflater, parent, false) }, { inflater, parent -> ItemSearchSuggestionSourceBinding.inflate(inflater, parent, false) },
@ -38,13 +24,6 @@ fun searchSuggestionSourceAD(
binding.textViewTitle.text = item.source.getTitle(context) binding.textViewTitle.text = item.source.getTitle(context)
binding.textViewSubtitle.text = item.source.getSummary(context) binding.textViewSubtitle.text = item.source.getSummary(context)
binding.switchLocal.isChecked = item.isEnabled binding.switchLocal.isChecked = item.isEnabled
val fallbackIcon = FaviconDrawable(context, R.style.FaviconDrawable_Small, item.source.name) binding.imageViewCover.setImageAsync(item.source)
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)
}
} }
} }

@ -1,27 +1,13 @@
package org.koitharu.kotatsu.search.ui.suggestion.adapter 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 com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.model.getSummary import org.koitharu.kotatsu.core.model.getSummary
import org.koitharu.kotatsu.core.model.getTitle 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.databinding.ItemSearchSuggestionSourceTipBinding
import org.koitharu.kotatsu.search.ui.suggestion.SearchSuggestionListener import org.koitharu.kotatsu.search.ui.suggestion.SearchSuggestionListener
import org.koitharu.kotatsu.search.ui.suggestion.model.SearchSuggestionItem import org.koitharu.kotatsu.search.ui.suggestion.model.SearchSuggestionItem
fun searchSuggestionSourceTipAD( fun searchSuggestionSourceTipAD(
coil: ImageLoader,
lifecycleOwner: LifecycleOwner,
listener: SearchSuggestionListener, listener: SearchSuggestionListener,
) = ) =
adapterDelegateViewBinding<SearchSuggestionItem.SourceTip, SearchSuggestionItem, ItemSearchSuggestionSourceTipBinding>( adapterDelegateViewBinding<SearchSuggestionItem.SourceTip, SearchSuggestionItem, ItemSearchSuggestionSourceTipBinding>(
@ -35,13 +21,6 @@ fun searchSuggestionSourceTipAD(
bind { bind {
binding.textViewTitle.text = item.source.getTitle(context) binding.textViewTitle.text = item.source.getTitle(context)
binding.textViewSubtitle.text = item.source.getSummary(context) binding.textViewSubtitle.text = item.source.getSummary(context)
val fallbackIcon = FaviconDrawable(context, R.style.FaviconDrawable_Small, item.source.name) binding.imageViewCover.setImageAsync(item.source)
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)
}
} }
} }

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

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

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

@ -6,33 +6,19 @@ import androidx.core.content.ContextCompat
import androidx.core.content.pm.ShortcutManagerCompat import androidx.core.content.pm.ShortcutManagerCompat
import androidx.core.view.isGone import androidx.core.view.isGone
import androidx.core.view.isVisible 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.adapterDelegate
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.model.getSummary import org.koitharu.kotatsu.core.model.getSummary
import org.koitharu.kotatsu.core.model.getTitle 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.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.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.ItemSourceConfigBinding
import org.koitharu.kotatsu.databinding.ItemTipBinding import org.koitharu.kotatsu.databinding.ItemTipBinding
import org.koitharu.kotatsu.settings.sources.model.SourceConfigItem import org.koitharu.kotatsu.settings.sources.model.SourceConfigItem
fun sourceConfigItemDelegate2( fun sourceConfigItemDelegate2(
listener: SourceConfigListener, listener: SourceConfigListener,
coil: ImageLoader,
lifecycleOwner: LifecycleOwner,
) = adapterDelegateViewBinding<SourceConfigItem.SourceItem, SourceConfigItem, ItemSourceConfigBinding>( ) = adapterDelegateViewBinding<SourceConfigItem.SourceItem, SourceConfigItem, ItemSourceConfigBinding>(
{ layoutInflater, parent -> { layoutInflater, parent ->
ItemSourceConfigBinding.inflate( ItemSourceConfigBinding.inflate(
@ -62,15 +48,7 @@ fun sourceConfigItemDelegate2(
binding.imageViewMenu.isVisible = item.isEnabled binding.imageViewMenu.isVisible = item.isEnabled
binding.textViewTitle.drawableStart = if (item.isPinned) iconPinned else null binding.textViewTitle.drawableStart = if (item.isPinned) iconPinned else null
binding.textViewDescription.text = item.source.getSummary(context) binding.textViewDescription.text = item.source.getSummary(context)
val fallbackIcon = FaviconDrawable(context, R.style.FaviconDrawable_Small, item.source.name) binding.imageViewIcon.setImageAsync(item.source)
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)
}
} }
} }

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

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

@ -1,8 +1,6 @@
package org.koitharu.kotatsu.settings.sources.catalog package org.koitharu.kotatsu.settings.sources.catalog
import android.content.Context import android.content.Context
import androidx.lifecycle.LifecycleOwner
import coil3.ImageLoader
import org.koitharu.kotatsu.core.model.getTitle import org.koitharu.kotatsu.core.model.getTitle
import org.koitharu.kotatsu.core.ui.BaseListAdapter import org.koitharu.kotatsu.core.ui.BaseListAdapter
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
@ -13,13 +11,11 @@ import org.koitharu.kotatsu.list.ui.model.ListModel
class SourcesCatalogAdapter( class SourcesCatalogAdapter(
listener: OnListItemClickListener<SourceCatalogItem.Source>, listener: OnListItemClickListener<SourceCatalogItem.Source>,
coil: ImageLoader,
lifecycleOwner: LifecycleOwner,
) : BaseListAdapter<ListModel>(), FastScroller.SectionIndexer { ) : BaseListAdapter<ListModel>(), FastScroller.SectionIndexer {
init { init {
addDelegate(ListItemType.CHAPTER_LIST, sourceCatalogItemSourceAD(coil, lifecycleOwner, listener)) addDelegate(ListItemType.CHAPTER_LIST, sourceCatalogItemSourceAD(listener))
addDelegate(ListItemType.HINT_EMPTY, sourceCatalogItemHintAD(coil, lifecycleOwner)) addDelegate(ListItemType.HINT_EMPTY, sourceCatalogItemHintAD())
addDelegate(ListItemType.STATE_LOADING, loadingStateAD()) 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.ui.util.ReversibleActionObserver
import org.koitharu.kotatsu.core.util.KotatsuColors import org.koitharu.kotatsu.core.util.KotatsuColors
import org.koitharu.kotatsu.core.util.ext.end 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.observe
import org.koitharu.kotatsu.core.util.ext.observeEvent import org.koitharu.kotatsu.core.util.ext.observeEvent
import org.koitharu.kotatsu.core.util.ext.setTextAndVisible import org.koitharu.kotatsu.core.util.ext.setTextAndVisible
@ -125,7 +123,7 @@ class StatsActivity : BaseActivity<ActivityStatsBinding>(),
marginStart = baseMargin + bars.start(v) marginStart = baseMargin + bars.start(v)
marginEnd = if (isTablet) baseMargin else baseMargin + bars.end(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) .setInsets(WindowInsetsCompat.Type.systemBars(), Insets.NONE)
.build() .build()
} }
@ -177,7 +175,7 @@ class StatsActivity : BaseActivity<ActivityStatsBinding>(),
override fun onInflate(stub: ViewStub?, inflated: View) { override fun onInflate(stub: ViewStub?, inflated: View) {
val stubBinding = ItemEmptyStateBinding.bind(inflated) 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.textPrimary.setText(R.string.text_empty_holder_primary)
stubBinding.textSecondary.setTextAndVisible(R.string.empty_stats_text) stubBinding.textSecondary.setTextAndVisible(R.string.empty_stats_text)
stubBinding.buttonRetry.isVisible = false stubBinding.buttonRetry.isVisible = false

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

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

@ -61,7 +61,7 @@ class FeedFragment :
override fun onViewBindingCreated(binding: FragmentListBinding, savedInstanceState: Bundle?) { override fun onViewBindingCreated(binding: FragmentListBinding, savedInstanceState: Bundle?) {
super.onViewBindingCreated(binding, savedInstanceState) super.onViewBindingCreated(binding, savedInstanceState)
val sizeResolver = StaticItemSizeResolver(resources.getDimensionPixelSize(R.dimen.smaller_grid_width)) 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) viewModel.onItemClick(item)
onItemClick(item.manga, v) onItemClick(item.manga, v)
} }

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

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

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

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

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

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

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

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

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

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

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

@ -13,7 +13,7 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:paddingBottom="?actionBarSize"> android:paddingBottom="?actionBarSize">
<com.google.android.material.imageview.ShapeableImageView <org.koitharu.kotatsu.image.ui.CoverImageView
android:id="@+id/imageView_cover" android:id="@+id/imageView_cover"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="0dp" android:layout_height="0dp"
@ -30,6 +30,7 @@
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
app:layout_constraintWidth_percent="0.3" app:layout_constraintWidth_percent="0.3"
app:shapeAppearanceOverlay="@style/ShapeAppearanceOverlay.Kotatsu.Cover" app:shapeAppearanceOverlay="@style/ShapeAppearanceOverlay.Kotatsu.Cover"
app:useExistingDrawable="true"
tools:background="@tools:sample/backgrounds/scenic[5]" tools:background="@tools:sample/backgrounds/scenic[5]"
tools:ignore="ContentDescription,UnusedAttribute" /> 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" app:cardBackgroundColor="?attr/colorSurfaceContainerHighest"
tools:layout_width="140dp"> tools:layout_width="140dp">
<org.koitharu.kotatsu.core.ui.widgets.CoverImageView <org.koitharu.kotatsu.image.ui.CoverImageView
android:id="@+id/imageView_thumb" android:id="@+id/imageView_thumb"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="horizontal" android:orientation="horizontal"
android:scaleType="centerCrop"
app:shapeAppearanceOverlay="@style/ShapeAppearanceOverlay.Kotatsu.Cover" app:shapeAppearanceOverlay="@style/ShapeAppearanceOverlay.Kotatsu.Cover"
tools:ignore="ContentDescription" tools:ignore="ContentDescription"
tools:src="@tools:sample/backgrounds/scenic[5]" /> tools:src="@tools:sample/backgrounds/scenic[5]" />

@ -6,61 +6,19 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:background="@drawable/list_selector" android:background="@drawable/list_selector"
android:minHeight="98dp" android:paddingVertical="4dp"
android:paddingStart="?android:listPreferredItemPaddingStart" android:paddingStart="?android:listPreferredItemPaddingStart"
tools:ignore="RtlSymmetry"> tools:ignore="RtlSymmetry">
<com.google.android.material.imageview.ShapeableImageView <org.koitharu.kotatsu.image.ui.CoverStackView
android:id="@+id/imageView_cover3" android:id="@+id/coversView"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="64dp" android:layout_height="@dimen/category_covers_height"
android:layout_marginStart="24dp" app:coverSize="3.4dp"
android:layout_marginBottom="12dp"
android:background="?attr/colorSecondaryContainer"
android:backgroundTintMode="src_atop"
android:scaleType="centerCrop"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintDimensionRatio="W,13:18" app:layout_constraintDimensionRatio="13:18"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="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" />
<TextView <TextView
android:id="@+id/textView_title" android:id="@+id/textView_title"
@ -74,7 +32,7 @@
android:textAppearance="?attr/textAppearanceBodyLarge" android:textAppearance="?attr/textAppearanceBodyLarge"
app:layout_constraintBottom_toTopOf="@id/textView_subtitle" app:layout_constraintBottom_toTopOf="@id/textView_subtitle"
app:layout_constraintEnd_toStartOf="@id/imageView_visible" 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_constraintTop_toTopOf="parent"
app:layout_constraintVertical_chainStyle="packed" /> app:layout_constraintVertical_chainStyle="packed" />
@ -91,7 +49,7 @@
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/imageView_visible" app:layout_constraintEnd_toStartOf="@id/imageView_visible"
app:layout_constraintHorizontal_bias="0" 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_constraintTop_toBottomOf="@id/textView_title"
app:layout_constraintVertical_chainStyle="packed" app:layout_constraintVertical_chainStyle="packed"
tools:text="@tools:sample/lorem[1]" /> tools:text="@tools:sample/lorem[1]" />

@ -6,61 +6,19 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:background="@drawable/list_selector" android:background="@drawable/list_selector"
android:minHeight="98dp" android:paddingVertical="4dp"
android:paddingStart="?android:listPreferredItemPaddingStart" android:paddingStart="?android:listPreferredItemPaddingStart"
tools:ignore="RtlSymmetry"> tools:ignore="RtlSymmetry">
<com.google.android.material.imageview.ShapeableImageView <org.koitharu.kotatsu.image.ui.CoverStackView
android:id="@+id/imageView_cover3" android:id="@+id/coversView"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="64dp" android:layout_height="@dimen/category_covers_height"
android:layout_marginStart="24dp" app:coverSize="3.4dp"
android:layout_marginBottom="12dp"
android:background="?attr/colorSecondaryContainer"
android:backgroundTintMode="src_atop"
android:scaleType="centerCrop"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintDimensionRatio="W,13:18" app:layout_constraintDimensionRatio="13:18"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="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" />
<TextView <TextView
android:id="@+id/textView_title" android:id="@+id/textView_title"
@ -73,7 +31,7 @@
android:textAppearance="?attr/textAppearanceBodyLarge" android:textAppearance="?attr/textAppearanceBodyLarge"
app:layout_constraintBottom_toTopOf="@id/textView_subtitle" app:layout_constraintBottom_toTopOf="@id/textView_subtitle"
app:layout_constraintEnd_toStartOf="@id/imageView_edit" 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_constraintTop_toTopOf="parent"
app:layout_constraintVertical_chainStyle="packed" app:layout_constraintVertical_chainStyle="packed"
tools:text="@tools:sample/lorem[1]" /> tools:text="@tools:sample/lorem[1]" />
@ -92,7 +50,7 @@
app:layout_constraintEnd_toStartOf="@id/imageView_tracker" app:layout_constraintEnd_toStartOf="@id/imageView_tracker"
app:layout_constraintHorizontal_bias="0" app:layout_constraintHorizontal_bias="0"
app:layout_constraintHorizontal_chainStyle="packed" 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_constraintTop_toBottomOf="@id/textView_title"
app:layout_constraintVertical_chainStyle="packed" app:layout_constraintVertical_chainStyle="packed"
tools:text="@tools:sample/lorem[1]" /> tools:text="@tools:sample/lorem[1]" />

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

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

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

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

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

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

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

@ -11,12 +11,11 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="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:id="@+id/imageView_cover"
android:layout_width="98dp" android:layout_width="98dp"
android:layout_height="0dp" android:layout_height="0dp"
android:background="?colorSurfaceContainer" android:background="?colorSurfaceContainer"
android:scaleType="centerCrop"
app:layout_constraintDimensionRatio="13:18" app:layout_constraintDimensionRatio="13:18"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"

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

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

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

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

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

Loading…
Cancel
Save