refactor: Switch to Coil3

This commit is contained in:
Ahmad Ansori Palembani 2024-05-23 09:04:02 +07:00
parent 2bffd8a653
commit 3412806bfc
Signed by: null2264
GPG key ID: BA64F8B60AF3EFB6
33 changed files with 286 additions and 233 deletions

View file

@ -256,9 +256,7 @@ dependencies {
implementation(libs.injekt.core) implementation(libs.injekt.core)
// Image library // Image library
implementation(libs.coil) implementation(libs.bundles.coil)
implementation(libs.coil.gif)
implementation(libs.coil.svg)
// Logging // Logging
implementation(libs.timber) implementation(libs.timber)
@ -327,7 +325,7 @@ tasks {
"-opt-in=androidx.compose.foundation.ExperimentalFoundationApi", "-opt-in=androidx.compose.foundation.ExperimentalFoundationApi",
"-opt-in=androidx.compose.animation.ExperimentalAnimationApi", "-opt-in=androidx.compose.animation.ExperimentalAnimationApi",
"-opt-in=androidx.compose.animation.graphics.ExperimentalAnimationGraphicsApi", "-opt-in=androidx.compose.animation.graphics.ExperimentalAnimationGraphicsApi",
"-opt-in=coil.annotation.ExperimentalCoilApi", "-opt-in=coil3.annotation.ExperimentalCoilApi",
"-opt-in=com.google.accompanist.permissions.ExperimentalPermissionsApi", "-opt-in=com.google.accompanist.permissions.ExperimentalPermissionsApi",
"-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi", "-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi",
"-opt-in=kotlinx.coroutines.FlowPreview", "-opt-in=kotlinx.coroutines.FlowPreview",

View file

@ -20,6 +20,9 @@ import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.ProcessLifecycleOwner import androidx.lifecycle.ProcessLifecycleOwner
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.multidex.MultiDex import androidx.multidex.MultiDex
import coil3.ImageLoader
import coil3.PlatformContext
import coil3.SingletonImageLoader
import dev.yokai.domain.AppState import dev.yokai.domain.AppState
import eu.kanade.tachiyomi.appwidget.TachiyomiWidgetManager import eu.kanade.tachiyomi.appwidget.TachiyomiWidgetManager
import eu.kanade.tachiyomi.data.image.coil.CoilSetup import eu.kanade.tachiyomi.data.image.coil.CoilSetup
@ -44,7 +47,7 @@ import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
import java.security.Security import java.security.Security
open class App : Application(), DefaultLifecycleObserver { open class App : Application(), DefaultLifecycleObserver, SingletonImageLoader.Factory {
val preferences: PreferencesHelper by injectLazy() val preferences: PreferencesHelper by injectLazy()
@ -71,7 +74,6 @@ open class App : Application(), DefaultLifecycleObserver {
Injekt.importModule(PreferenceModule(this)) Injekt.importModule(PreferenceModule(this))
Injekt.importModule(AppModule(this)) Injekt.importModule(AppModule(this))
CoilSetup(this)
setupNotificationChannels() setupNotificationChannels()
ProcessLifecycleOwner.get().lifecycle.addObserver(this) ProcessLifecycleOwner.get().lifecycle.addObserver(this)
@ -171,6 +173,10 @@ open class App : Application(), DefaultLifecycleObserver {
} }
} }
} }
override fun newImageLoader(context: PlatformContext): ImageLoader {
return CoilSetup.setup(context)
}
} }
private const val ACTION_DISABLE_INCOGNITO_MODE = "tachi.action.DISABLE_INCOGNITO_MODE" private const val ACTION_DISABLE_INCOGNITO_MODE = "tachi.action.DISABLE_INCOGNITO_MODE"

View file

@ -16,13 +16,14 @@ import androidx.glance.appwidget.provideContent
import androidx.glance.appwidget.updateAll import androidx.glance.appwidget.updateAll
import androidx.glance.background import androidx.glance.background
import androidx.glance.layout.fillMaxSize import androidx.glance.layout.fillMaxSize
import coil.executeBlocking import coil3.executeBlocking
import coil.imageLoader import coil3.imageLoader
import coil.request.CachePolicy import coil3.request.CachePolicy
import coil.request.ImageRequest import coil3.request.ImageRequest
import coil.size.Precision import coil3.request.transformations
import coil.size.Scale import coil3.size.Precision
import coil.transform.RoundedCornersTransformation import coil3.size.Scale
import coil3.transform.RoundedCornersTransformation
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.appwidget.components.CoverHeight import eu.kanade.tachiyomi.appwidget.components.CoverHeight
import eu.kanade.tachiyomi.appwidget.components.CoverWidth import eu.kanade.tachiyomi.appwidget.components.CoverWidth
@ -109,7 +110,7 @@ class UpdatesGridGlanceWidget : GlanceAppWidget() {
} }
} }
.build() .build()
Pair(updatesView.id!!, app.imageLoader.executeBlocking(request).drawable?.toBitmap()) Pair(updatesView.id!!, app.imageLoader.executeBlocking(request).image?.asDrawable(app.resources)?.toBitmap())
} }
} }

View file

@ -2,8 +2,8 @@ package eu.kanade.tachiyomi.data.cache
import android.content.Context import android.content.Context
import android.text.format.Formatter import android.text.format.Formatter
import coil.imageLoader import coil3.imageLoader
import coil.memory.MemoryCache import coil3.memory.MemoryCache
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.DatabaseHelper import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.Manga

View file

@ -2,47 +2,38 @@ package eu.kanade.tachiyomi.data.image.coil
import android.app.ActivityManager import android.app.ActivityManager
import android.content.Context import android.content.Context
import android.os.Build
import androidx.core.content.getSystemService import androidx.core.content.getSystemService
import coil.Coil import coil3.ImageLoader
import coil.ImageLoader import coil3.disk.DiskCache
import coil.decode.GifDecoder import coil3.disk.directory
import coil.decode.ImageDecoderDecoder import coil3.memory.MemoryCache
import coil.disk.DiskCache import coil3.network.okhttp.OkHttpNetworkFetcherFactory
import coil.memory.MemoryCache import coil3.request.allowHardware
import coil.util.DebugLogger import coil3.request.allowRgb565
import eu.kanade.tachiyomi.BuildConfig import coil3.request.crossfade
import eu.kanade.tachiyomi.network.NetworkHelper import eu.kanade.tachiyomi.network.NetworkHelper
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
class CoilSetup(context: Context) { class CoilSetup {
init { companion object {
val imageLoader = ImageLoader.Builder(context).apply { fun setup(context: Context): ImageLoader {
val callFactoryInit = { Injekt.get<NetworkHelper>().client } return ImageLoader.Builder(context).apply {
val diskCacheInit = { CoilDiskCache.get(context) } val callFactoryLazy = lazy { Injekt.get<NetworkHelper>().client }
components { val diskCacheLazy = lazy { CoilDiskCache.get(context) }
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { components {
add(ImageDecoderDecoder.Factory()) add(OkHttpNetworkFetcherFactory(callFactoryLazy::value))
} else { add(TachiyomiImageDecoder.Factory())
add(GifDecoder.Factory()) add(MangaCoverFetcher.Factory(callFactoryLazy, diskCacheLazy))
add(MangaCoverKeyer())
} }
add(TachiyomiImageDecoder.Factory()) diskCache(diskCacheLazy::value)
add(MangaCoverFetcher.Factory(lazy(callFactoryInit), lazy(diskCacheInit))) memoryCache { MemoryCache.Builder().maxSizePercent(context, 0.40).build() }
add(MangaCoverKeyer()) crossfade(true)
add(InputStreamFetcher.Factory()) allowRgb565(context.getSystemService<ActivityManager>()!!.isLowRamDevice)
} allowHardware(true)
callFactory(callFactoryInit) }.build()
diskCache(diskCacheInit) }
memoryCache { MemoryCache.Builder(context).maxSizePercent(0.40).build() }
crossfade(true)
allowRgb565(context.getSystemService<ActivityManager>()!!.isLowRamDevice)
allowHardware(true)
if (BuildConfig.DEBUG) {
logger(DebugLogger())
}
}.build()
Coil.setImageLoader(imageLoader)
} }
} }

View file

@ -5,7 +5,8 @@ import android.view.View
import android.widget.ImageView import android.widget.ImageView
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.vectordrawable.graphics.drawable.VectorDrawableCompat import androidx.vectordrawable.graphics.drawable.VectorDrawableCompat
import coil.target.ImageViewTarget import coil3.Image
import coil3.target.ImageViewTarget
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.util.system.getResourceColor import eu.kanade.tachiyomi.util.system.getResourceColor
@ -15,7 +16,9 @@ class CoverViewTarget(
val scaleType: ImageView.ScaleType = ImageView.ScaleType.CENTER_CROP, val scaleType: ImageView.ScaleType = ImageView.ScaleType.CENTER_CROP,
) : ImageViewTarget(view) { ) : ImageViewTarget(view) {
override fun onError(error: Drawable?) { override fun onError(error: Image?) {
//val drawable = error?.asDrawable(view.context.resources)
progress?.isVisible = false progress?.isVisible = false
view.scaleType = ImageView.ScaleType.CENTER view.scaleType = ImageView.ScaleType.CENTER
val vector = VectorDrawableCompat.create( val vector = VectorDrawableCompat.create(
@ -27,13 +30,17 @@ class CoverViewTarget(
view.setImageDrawable(vector) view.setImageDrawable(vector)
} }
override fun onStart(placeholder: Drawable?) { override fun onStart(placeholder: Image?) {
//val drawable = placeholder?.asDrawable(view.context.resources)
progress?.isVisible = true progress?.isVisible = true
view.scaleType = scaleType view.scaleType = scaleType
super.onStart(placeholder) super.onStart(placeholder)
} }
override fun onSuccess(result: Drawable) { override fun onSuccess(result: Image) {
//val drawable = result?.asDrawable(view.context.resources)
progress?.isVisible = false progress?.isVisible = false
view.scaleType = scaleType view.scaleType = scaleType
super.onSuccess(result) super.onSuccess(result)

View file

@ -1,15 +1,15 @@
package eu.kanade.tachiyomi.data.image.coil package eu.kanade.tachiyomi.data.image.coil
import android.graphics.BitmapFactory import android.graphics.BitmapFactory
import android.graphics.drawable.Drawable
import android.widget.ImageView import android.widget.ImageView
import androidx.palette.graphics.Palette import androidx.palette.graphics.Palette
import coil.ImageLoader import coil3.Image
import coil.imageLoader import coil3.ImageLoader
import coil.memory.MemoryCache import coil3.imageLoader
import coil.request.Disposable import coil3.memory.MemoryCache
import coil.request.ImageRequest import coil3.request.Disposable
import coil.target.ImageViewTarget import coil3.request.ImageRequest
import coil3.target.ImageViewTarget
import eu.kanade.tachiyomi.data.cache.CoverCache import eu.kanade.tachiyomi.data.cache.CoverCache
import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.util.system.launchIO import eu.kanade.tachiyomi.util.system.launchIO
@ -22,7 +22,7 @@ class LibraryMangaImageTarget(
private val coverCache: CoverCache by injectLazy() private val coverCache: CoverCache by injectLazy()
override fun onError(error: Drawable?) { override fun onError(error: Image?) {
super.onError(error) super.onError(error)
if (manga.favorite) { if (manga.favorite) {
launchIO { launchIO {

View file

@ -1,16 +1,16 @@
package eu.kanade.tachiyomi.data.image.coil package eu.kanade.tachiyomi.data.image.coil
import android.webkit.MimeTypeMap import android.webkit.MimeTypeMap
import coil.ImageLoader import coil3.Extras
import coil.decode.DataSource import coil3.ImageLoader
import coil.decode.ImageSource import coil3.decode.DataSource
import coil.disk.DiskCache import coil3.decode.ImageSource
import coil.fetch.FetchResult import coil3.disk.DiskCache
import coil.fetch.Fetcher import coil3.fetch.FetchResult
import coil.fetch.SourceResult import coil3.fetch.Fetcher
import coil.network.HttpException import coil3.fetch.SourceFetchResult
import coil.request.Options import coil3.getOrDefault
import coil.request.Parameters import coil3.request.Options
import eu.kanade.tachiyomi.data.cache.CoverCache import eu.kanade.tachiyomi.data.cache.CoverCache
import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.network.await import eu.kanade.tachiyomi.network.await
@ -26,17 +26,16 @@ import okhttp3.Call
import okhttp3.Request import okhttp3.Request
import okhttp3.Response import okhttp3.Response
import okhttp3.internal.closeQuietly import okhttp3.internal.closeQuietly
import okio.FileSystem
import okio.Path.Companion.toOkioPath import okio.Path.Companion.toOkioPath
import okio.Source import okio.Source
import okio.buffer import okio.buffer
import okio.sink import okio.sink
import okio.source
import timber.log.Timber import timber.log.Timber
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
import java.io.File import java.io.File
import java.net.HttpURLConnection import java.net.HttpURLConnection
import java.util.Date import java.util.*
class MangaCoverFetcher( class MangaCoverFetcher(
private val manga: Manga, private val manga: Manga,
@ -71,7 +70,7 @@ class MangaCoverFetcher(
val networkRead = options.networkCachePolicy.readEnabled val networkRead = options.networkCachePolicy.readEnabled
val onlyCache = !networkRead && diskRead val onlyCache = !networkRead && diskRead
val shouldFetchRemotely = networkRead && !diskRead && !onlyCache val shouldFetchRemotely = networkRead && !diskRead && !onlyCache
val useCustomCover = options.parameters.value(useCustomCover) ?: true val useCustomCover = options.extras.getOrDefault(USE_CUSTOM_COVER_KEY)
// Use custom cover if exists // Use custom cover if exists
if (!shouldFetchRemotely) { if (!shouldFetchRemotely) {
val customCoverFile by lazy { coverCache.getCustomCoverFile(manga) } val customCoverFile by lazy { coverCache.getCustomCoverFile(manga) }
@ -101,7 +100,7 @@ class MangaCoverFetcher(
// Read from snapshot // Read from snapshot
setRatioAndColorsInScope(manga) setRatioAndColorsInScope(manga)
return SourceResult( return SourceFetchResult(
source = snapshot.toImageSource(), source = snapshot.toImageSource(),
mimeType = "image/*", mimeType = "image/*",
dataSource = DataSource.DISK, dataSource = DataSource.DISK,
@ -122,7 +121,7 @@ class MangaCoverFetcher(
// Read from disk cache // Read from disk cache
snapshot = writeToDiskCache(snapshot, response) snapshot = writeToDiskCache(snapshot, response)
if (snapshot != null) { if (snapshot != null) {
return SourceResult( return SourceFetchResult(
source = snapshot.toImageSource(), source = snapshot.toImageSource(),
mimeType = "image/*", mimeType = "image/*",
dataSource = DataSource.NETWORK, dataSource = DataSource.NETWORK,
@ -130,8 +129,8 @@ class MangaCoverFetcher(
} }
// Read from response if cache is unused or unusable // Read from response if cache is unused or unusable
return SourceResult( return SourceFetchResult(
source = ImageSource(source = responseBody.source(), context = options.context), source = ImageSource(source = responseBody.source(), fileSystem = FileSystem.SYSTEM),
mimeType = "image/*", mimeType = "image/*",
dataSource = if (response.cacheResponse != null) DataSource.DISK else DataSource.NETWORK, dataSource = if (response.cacheResponse != null) DataSource.DISK else DataSource.NETWORK,
) )
@ -149,18 +148,20 @@ class MangaCoverFetcher(
val client = sourceLazy.value?.client ?: callFactoryLazy.value val client = sourceLazy.value?.client ?: callFactoryLazy.value
val response = client.newCall(newRequest()).await() val response = client.newCall(newRequest()).await()
if (!response.isSuccessful && response.code != HttpURLConnection.HTTP_NOT_MODIFIED) { if (!response.isSuccessful && response.code != HttpURLConnection.HTTP_NOT_MODIFIED) {
response.body?.closeQuietly() response.body.closeQuietly()
throw HttpException(response) throw Exception(response.message) // FIXME: Should probably use something else other than generic Exception
} }
return response return response
} }
private fun newRequest(): Request { private fun newRequest(): Request {
val request = Request.Builder() val request = Request.Builder().apply {
.url(url) url(url)
.headers(sourceLazy.value?.headers ?: options.headers)
// Support attaching custom data to the network request. val sourceHeaders = sourceLazy.value?.headers
.tag(Parameters::class.java, options.parameters) if (sourceHeaders != null)
headers(sourceHeaders)
}
val diskRead = options.diskCachePolicy.readEnabled val diskRead = options.diskCachePolicy.readEnabled
val networkRead = options.networkCachePolicy.readEnabled val networkRead = options.networkCachePolicy.readEnabled
@ -227,7 +228,11 @@ class MangaCoverFetcher(
} }
private fun readFromDiskCache(): DiskCache.Snapshot? { private fun readFromDiskCache(): DiskCache.Snapshot? {
return if (options.diskCachePolicy.readEnabled) diskCacheLazy.value[diskCacheKey!!] else null return if (options.diskCachePolicy.readEnabled) {
diskCacheLazy.value.openSnapshot(diskCacheKey!!)
} else {
null
}
} }
private fun writeToDiskCache( private fun writeToDiskCache(
@ -239,15 +244,15 @@ class MangaCoverFetcher(
return null return null
} }
val editor = if (snapshot != null) { val editor = if (snapshot != null) {
snapshot.closeAndEdit() snapshot.closeAndOpenEditor()
} else { } else {
diskCacheLazy.value.edit(diskCacheKey!!) diskCacheLazy.value.openEditor(diskCacheKey!!)
} ?: return null } ?: return null
try { try {
diskCacheLazy.value.fileSystem.write(editor.data) { diskCacheLazy.value.fileSystem.write(editor.data) {
response.body!!.source().readAll(this) response.body.source().readAll(this)
} }
return editor.commitAndGet() return editor.commitAndOpenSnapshot()
} catch (e: Exception) { } catch (e: Exception) {
try { try {
editor.abort() editor.abort()
@ -258,7 +263,12 @@ class MangaCoverFetcher(
} }
private fun DiskCache.Snapshot.toImageSource(): ImageSource { private fun DiskCache.Snapshot.toImageSource(): ImageSource {
return ImageSource(file = data, diskCacheKey = diskCacheKey, closeable = this) return ImageSource(
file = data,
fileSystem = FileSystem.SYSTEM,
diskCacheKey = diskCacheKey,
closeable = this,
)
} }
private fun setRatioAndColorsInScope(manga: Manga, ogFile: File? = null, force: Boolean = false) { private fun setRatioAndColorsInScope(manga: Manga, ogFile: File? = null, force: Boolean = false) {
@ -283,8 +293,12 @@ class MangaCoverFetcher(
} }
private fun fileLoader(file: File): FetchResult { private fun fileLoader(file: File): FetchResult {
return SourceResult( return SourceFetchResult(
source = ImageSource(file = file.toOkioPath(), diskCacheKey = diskCacheKey), source = ImageSource(
file = file.toOkioPath(),
fileSystem = FileSystem.SYSTEM,
diskCacheKey = diskCacheKey,
),
mimeType = "image/*", mimeType = "image/*",
dataSource = DataSource.DISK, dataSource = DataSource.DISK,
) )
@ -318,7 +332,7 @@ class MangaCoverFetcher(
} }
companion object { companion object {
const val useCustomCover = "use_custom_cover" val USE_CUSTOM_COVER_KEY = Extras.Key(true)
private val CACHE_CONTROL_FORCE_NETWORK_NO_CACHE = CacheControl.Builder().noCache().noStore().build() private val CACHE_CONTROL_FORCE_NETWORK_NO_CACHE = CacheControl.Builder().noCache().noStore().build()
private val CACHE_CONTROL_NO_NETWORK_NO_CACHE = CacheControl.Builder().noCache().onlyIfCached().build() private val CACHE_CONTROL_NO_NETWORK_NO_CACHE = CacheControl.Builder().noCache().onlyIfCached().build()

View file

@ -1,7 +1,7 @@
package eu.kanade.tachiyomi.data.image.coil package eu.kanade.tachiyomi.data.image.coil
import coil.key.Keyer import coil3.key.Keyer
import coil.request.Options import coil3.request.Options
import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.util.storage.DiskUtil import eu.kanade.tachiyomi.util.storage.DiskUtil

View file

@ -2,11 +2,15 @@ package eu.kanade.tachiyomi.data.image.coil
import android.graphics.Bitmap import android.graphics.Bitmap
import android.os.Build import android.os.Build
import androidx.core.graphics.drawable.toDrawable import coil3.ImageLoader
import coil.ImageLoader import coil3.asCoilImage
import coil.decode.* import coil3.decode.DecodeResult
import coil.fetch.SourceResult import coil3.decode.DecodeUtils
import coil.request.Options import coil3.decode.Decoder
import coil3.decode.ImageSource
import coil3.fetch.SourceFetchResult
import coil3.request.Options
import coil3.request.bitmapConfig
import eu.kanade.tachiyomi.util.system.GLUtil import eu.kanade.tachiyomi.util.system.GLUtil
import eu.kanade.tachiyomi.util.system.ImageUtil import eu.kanade.tachiyomi.util.system.ImageUtil
import okio.BufferedSource import okio.BufferedSource
@ -79,16 +83,16 @@ class TachiyomiImageDecoder(private val resources: ImageSource, private val opti
*/ */
return DecodeResult( return DecodeResult(
drawable = bitmap.toDrawable(options.context.resources), image = bitmap.asCoilImage(),
isSampled = sampleSize > 1, isSampled = sampleSize > 1,
) )
} }
class Factory : Decoder.Factory { class Factory : Decoder.Factory {
override fun create(result: SourceResult, options: Options, imageLoader: ImageLoader): Decoder? { override fun create(result: SourceFetchResult, options: Options, imageLoader: ImageLoader): Decoder? {
if (isApplicable(result.source.source()) || options.customDecoder) return TachiyomiImageDecoder(result.source, options) if (!isApplicable(result.source.source())) return null
return null return TachiyomiImageDecoder(result.source, options)
} }
private fun isApplicable(source: BufferedSource): Boolean { private fun isApplicable(source: BufferedSource): Boolean {

View file

@ -1,14 +1,14 @@
package eu.kanade.tachiyomi.data.image.coil package eu.kanade.tachiyomi.data.image.coil
import android.graphics.Bitmap import coil3.Extras
import android.os.Build import coil3.getExtra
import coil.request.ImageRequest import coil3.request.ImageRequest
import coil.request.Options import coil3.request.Options
import coil.size.Dimension import coil3.size.Dimension
import coil.size.Scale import coil3.size.Scale
import coil.size.Size import coil3.size.Size
import coil.size.isOriginal import coil3.size.isOriginal
import coil.size.pxOrElse import coil3.size.pxOrElse
internal inline fun Size.widthPx(scale: Scale, original: () -> Int): Int { internal inline fun Size.widthPx(scale: Scale, original: () -> Int): Int {
return if (isOriginal) original() else width.toPx(scale) return if (isOriginal) original() else width.toPx(scale)
@ -26,22 +26,19 @@ internal fun Dimension.toPx(scale: Scale): Int = pxOrElse {
} }
fun ImageRequest.Builder.cropBorders(enable: Boolean) = apply { fun ImageRequest.Builder.cropBorders(enable: Boolean) = apply {
setParameter(cropBordersKey, enable) extras[cropBordersKey] = enable
} }
val Options.cropBorders: Boolean val Options.cropBorders: Boolean
get() = parameters.value(cropBordersKey) ?: false get() = getExtra(cropBordersKey)
private val cropBordersKey = "crop_borders" private val cropBordersKey = Extras.Key(default = false)
fun ImageRequest.Builder.customDecoder(enable: Boolean) = apply { fun ImageRequest.Builder.customDecoder(enable: Boolean) = apply {
setParameter(customDecoderKey, enable) extras[customDecoderKey] = enable
} }
val Options.customDecoder: Boolean val Options.customDecoder: Boolean
get() = parameters.value(customDecoderKey) ?: false get() = getExtra(customDecoderKey)
private val customDecoderKey = "custom_decoder" private val customDecoderKey = Extras.Key(default = false)
val Options.bitmapConfig: Bitmap.Config
get() = this.config

View file

@ -16,9 +16,9 @@ import androidx.work.WorkInfo
import androidx.work.WorkManager import androidx.work.WorkManager
import androidx.work.WorkQuery import androidx.work.WorkQuery
import androidx.work.WorkerParameters import androidx.work.WorkerParameters
import coil.Coil import coil3.imageLoader
import coil.request.CachePolicy import coil3.request.CachePolicy
import coil.request.ImageRequest import coil3.request.ImageRequest
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.cache.CoverCache import eu.kanade.tachiyomi.data.cache.CoverCache
import eu.kanade.tachiyomi.data.database.DatabaseHelper import eu.kanade.tachiyomi.data.database.DatabaseHelper
@ -261,21 +261,19 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
val thumbnailUrl = manga.thumbnail_url val thumbnailUrl = manga.thumbnail_url
manga.copyFrom(networkManga) manga.copyFrom(networkManga)
manga.initialized = true manga.initialized = true
if (thumbnailUrl != manga.thumbnail_url) { val request: ImageRequest =
coverCache.deleteFromCache(thumbnailUrl) if (thumbnailUrl != manga.thumbnail_url) {
// load new covers in background coverCache.deleteFromCache(thumbnailUrl)
val request = // load new covers in background
ImageRequest.Builder(context).data(manga) ImageRequest.Builder(context).data(manga)
.memoryCachePolicy(CachePolicy.DISABLED).build() .memoryCachePolicy(CachePolicy.DISABLED).build()
Coil.imageLoader(context).execute(request) } else {
} else {
val request =
ImageRequest.Builder(context).data(manga) ImageRequest.Builder(context).data(manga)
.memoryCachePolicy(CachePolicy.DISABLED) .memoryCachePolicy(CachePolicy.DISABLED)
.diskCachePolicy(CachePolicy.WRITE_ONLY) .diskCachePolicy(CachePolicy.WRITE_ONLY)
.build() .build()
Coil.imageLoader(context).execute(request) }
} context.imageLoader.execute(request)
db.insertManga(manga).executeAsBlocking() db.insertManga(manga).executeAsBlocking()
} }
} }

View file

@ -13,10 +13,11 @@ import androidx.core.app.ActivityCompat
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat import androidx.core.app.NotificationManagerCompat
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import coil.Coil import coil3.imageLoader
import coil.request.CachePolicy import coil3.request.CachePolicy
import coil.request.ImageRequest import coil3.request.ImageRequest
import coil.transform.CircleCropTransformation import coil3.request.transformations
import coil3.transform.CircleCropTransformation
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.Chapter import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.data.database.models.LibraryManga import eu.kanade.tachiyomi.data.database.models.LibraryManga
@ -191,11 +192,11 @@ class LibraryUpdateNotifier(private val context: Context) {
.transformations(CircleCropTransformation()) .transformations(CircleCropTransformation())
.size(width = ICON_SIZE, height = ICON_SIZE).build() .size(width = ICON_SIZE, height = ICON_SIZE).build()
Coil.imageLoader(context) context.imageLoader
.execute(request).drawable?.let { drawable -> .execute(request).image?.asDrawable(context.resources)?.let { drawable ->
setLargeIcon((drawable as? BitmapDrawable)?.bitmap) setLargeIcon((drawable as? BitmapDrawable)?.bitmap)
} }
} catch (e: Exception) { } catch (_: Exception) {
} }
setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_SUMMARY) setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_SUMMARY)
setContentTitle(manga.title) setContentTitle(manga.title)

View file

@ -9,8 +9,8 @@ import androidx.core.text.color
import androidx.core.text.scale import androidx.core.text.scale
import androidx.core.view.isGone import androidx.core.view.isGone
import androidx.core.view.isVisible import androidx.core.view.isVisible
import coil.dispose import coil3.dispose
import coil.load import coil3.load
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.image.coil.CoverViewTarget import eu.kanade.tachiyomi.data.image.coil.CoverViewTarget
import eu.kanade.tachiyomi.databinding.ExtensionCardItemBinding import eu.kanade.tachiyomi.databinding.ExtensionCardItemBinding

View file

@ -10,9 +10,9 @@ import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.core.view.marginBottom import androidx.core.view.marginBottom
import androidx.core.view.updateLayoutParams import androidx.core.view.updateLayoutParams
import coil.dispose import coil3.dispose
import coil.size.Precision import coil3.size.Precision
import coil.size.Scale import coil3.size.Scale
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.image.coil.loadManga import eu.kanade.tachiyomi.data.image.coil.loadManga

View file

@ -4,7 +4,7 @@ import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.core.view.updateLayoutParams import androidx.core.view.updateLayoutParams
import coil.dispose import coil3.dispose
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.image.coil.loadManga import eu.kanade.tachiyomi.data.image.coil.loadManga
import eu.kanade.tachiyomi.databinding.MangaListItemBinding import eu.kanade.tachiyomi.databinding.MangaListItemBinding

View file

@ -13,8 +13,7 @@ import android.view.inputmethod.InputMethodManager
import androidx.core.graphics.ColorUtils import androidx.core.graphics.ColorUtils
import androidx.core.view.children import androidx.core.view.children
import androidx.core.view.isVisible import androidx.core.view.isVisible
import coil.load import coil3.load
import coil.request.Parameters
import com.google.android.material.chip.Chip import com.google.android.material.chip.Chip
import com.google.android.material.chip.ChipGroup import com.google.android.material.chip.ChipGroup
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
@ -215,10 +214,9 @@ class EditMangaDialog : DialogController {
binding.resetCover.setOnClickListener { binding.resetCover.setOnClickListener {
binding.mangaCover.load( binding.mangaCover.load(
manga, manga,
builder = { ) {
parameters(Parameters.Builder().set(MangaCoverFetcher.useCustomCover, false).build()) extras[MangaCoverFetcher.USE_CUSTOM_COVER_KEY] = false
}, }
)
customCoverUri = null customCoverUri = null
willResetCover = true willResetCover = true
} }

View file

@ -7,6 +7,7 @@ import android.app.PendingIntent
import android.content.ClipData import android.content.ClipData
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.graphics.Bitmap
import android.graphics.Color import android.graphics.Color
import android.graphics.drawable.BitmapDrawable import android.graphics.drawable.BitmapDrawable
import android.graphics.drawable.Icon import android.graphics.drawable.Icon
@ -40,8 +41,9 @@ import androidx.palette.graphics.Palette
import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import coil.imageLoader import coil3.imageLoader
import coil.request.ImageRequest import coil3.request.ImageRequest
import coil3.request.allowHardware
import com.bluelinelabs.conductor.ControllerChangeHandler import com.bluelinelabs.conductor.ControllerChangeHandler
import com.bluelinelabs.conductor.ControllerChangeType import com.bluelinelabs.conductor.ControllerChangeType
import com.google.android.material.chip.Chip import com.google.android.material.chip.Chip
@ -569,8 +571,20 @@ class MangaDetailsController :
val request = ImageRequest.Builder(view.context).data(presenter.manga).allowHardware(false) val request = ImageRequest.Builder(view.context).data(presenter.manga).allowHardware(false)
.memoryCacheKey(presenter.manga.key()) .memoryCacheKey(presenter.manga.key())
.target( .target(
onSuccess = { drawable -> onSuccess = { image ->
val bitmap = (drawable as? BitmapDrawable)?.bitmap val drawable = image.asDrawable(view.context.resources)
val copy = (drawable as? BitmapDrawable)?.let {
BitmapDrawable(
view.context.resources,
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
it.bitmap.copy(Bitmap.Config.HARDWARE, false)
else
it.bitmap.copy(it.bitmap.config, false),
)
} ?: drawable
val bitmap = (copy as? BitmapDrawable)?.bitmap
// Generate the Palette on a background thread. // Generate the Palette on a background thread.
if (bitmap != null) { if (bitmap != null) {
Palette.from(bitmap).generate { Palette.from(bitmap).generate {
@ -590,7 +604,7 @@ class MangaDetailsController :
} }
} }
} }
binding.mangaCoverFull.setImageDrawable(drawable) binding.mangaCoverFull.setImageDrawable(copy)
getHeader()?.updateCover(manga!!) getHeader()?.updateCover(manga!!)
}, },
onError = { onError = {

View file

@ -4,12 +4,11 @@ import android.app.Application
import android.graphics.Bitmap import android.graphics.Bitmap
import android.net.Uri import android.net.Uri
import android.os.Environment import android.os.Environment
import coil.Coil import coil3.imageLoader
import coil.imageLoader import coil3.memory.MemoryCache
import coil.memory.MemoryCache import coil3.request.CachePolicy
import coil.request.CachePolicy import coil3.request.ImageRequest
import coil.request.ImageRequest import coil3.request.SuccessResult
import coil.request.SuccessResult
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.cache.CoverCache import eu.kanade.tachiyomi.data.cache.CoverCache
import eu.kanade.tachiyomi.data.database.DatabaseHelper import eu.kanade.tachiyomi.data.database.DatabaseHelper
@ -379,7 +378,7 @@ class MangaDetailsPresenter(
.diskCachePolicy(CachePolicy.WRITE_ONLY) .diskCachePolicy(CachePolicy.WRITE_ONLY)
.build() .build()
if (Coil.imageLoader(preferences.context).execute(request) is SuccessResult) { if (preferences.context.imageLoader.execute(request) is SuccessResult) {
preferences.context.imageLoader.memoryCache?.remove(MemoryCache.Key(manga.key())) preferences.context.imageLoader.memoryCache?.remove(MemoryCache.Key(manga.key()))
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
view?.setPaletteColor() view?.setPaletteColor()

View file

@ -25,7 +25,9 @@ import androidx.core.view.updateLayoutParams
import androidx.core.widget.TextViewCompat import androidx.core.widget.TextViewCompat
import androidx.transition.TransitionSet import androidx.transition.TransitionSet
import androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat import androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat
import coil.request.CachePolicy import coil3.request.CachePolicy
import coil3.request.placeholder
import coil3.request.error
import com.google.android.material.button.MaterialButton import com.google.android.material.button.MaterialButton
import com.google.android.material.chip.Chip import com.google.android.material.chip.Chip
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
@ -289,7 +291,7 @@ class MangaHeaderHolder(
} }
} }
@SuppressLint("SetTextI18n") @SuppressLint("SetTextI18n", "StringFormatInvalid")
fun bind(item: MangaHeaderItem, manga: Manga) { fun bind(item: MangaHeaderItem, manga: Manga) {
val presenter = adapter.delegate.mangaPresenter() val presenter = adapter.delegate.mangaPresenter()
if (binding == null) { if (binding == null) {
@ -680,9 +682,10 @@ class MangaHeaderHolder(
diskCachePolicy(CachePolicy.READ_ONLY) diskCachePolicy(CachePolicy.READ_ONLY)
target( target(
onSuccess = { onSuccess = {
val bitmap = (it as? BitmapDrawable)?.bitmap val result = it.asDrawable(itemView.resources)
val bitmap = (result as? BitmapDrawable)?.bitmap
if (bitmap == null) { if (bitmap == null) {
binding.backdrop.setImageDrawable(it) binding.backdrop.setImageDrawable(result)
return@target return@target
} }
val yOffset = (bitmap.height / 2 * 0.33).toInt() val yOffset = (bitmap.height / 2 * 0.33).toInt()

View file

@ -2,8 +2,9 @@ package eu.kanade.tachiyomi.ui.manga.track
import android.view.View import android.view.View
import androidx.core.view.isVisible import androidx.core.view.isVisible
import coil.dispose import coil3.dispose
import coil.load import coil3.load
import coil3.request.allowHardware
import com.google.android.material.shape.CornerFamily import com.google.android.material.shape.CornerFamily
import com.mikepenz.fastadapter.FastAdapter import com.mikepenz.fastadapter.FastAdapter
import com.mikepenz.fastadapter.items.AbstractItem import com.mikepenz.fastadapter.items.AbstractItem

View file

@ -2,7 +2,7 @@ package eu.kanade.tachiyomi.ui.migration
import android.view.View import android.view.View
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import coil.dispose import coil3.dispose
import eu.davidea.flexibleadapter.FlexibleAdapter import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.IFlexible import eu.davidea.flexibleadapter.items.IFlexible
import eu.kanade.tachiyomi.data.image.coil.loadManga import eu.kanade.tachiyomi.data.image.coil.loadManga

View file

@ -5,8 +5,8 @@ import androidx.appcompat.widget.PopupMenu
import androidx.constraintlayout.widget.ConstraintLayout import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.view.isInvisible import androidx.core.view.isInvisible
import androidx.core.view.isVisible import androidx.core.view.isVisible
import coil.Coil import coil3.imageLoader
import coil.request.ImageRequest import coil3.request.ImageRequest
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.DatabaseHelper import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.Manga
@ -20,6 +20,7 @@ import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder
import eu.kanade.tachiyomi.ui.library.setFreeformCoverRatio import eu.kanade.tachiyomi.ui.library.setFreeformCoverRatio
import eu.kanade.tachiyomi.ui.manga.MangaDetailsController import eu.kanade.tachiyomi.ui.manga.MangaDetailsController
import eu.kanade.tachiyomi.util.system.launchUI import eu.kanade.tachiyomi.util.system.launchUI
import eu.kanade.tachiyomi.util.system.setExtras
import eu.kanade.tachiyomi.util.view.setCards import eu.kanade.tachiyomi.util.view.setCards
import eu.kanade.tachiyomi.util.view.setVectorCompat import eu.kanade.tachiyomi.util.view.setVectorCompat
import eu.kanade.tachiyomi.util.view.withFadeTransaction import eu.kanade.tachiyomi.util.view.withFadeTransaction
@ -147,9 +148,9 @@ class MigrationProcessHolder(
val request = ImageRequest.Builder(view.context).data(manga) val request = ImageRequest.Builder(view.context).data(manga)
.target(CoverViewTarget(coverThumbnail, progress)) .target(CoverViewTarget(coverThumbnail, progress))
.setParameter(MangaCoverFetcher.useCustomCover, false) .setExtras(MangaCoverFetcher.USE_CUSTOM_COVER_KEY, false)
.build() .build()
Coil.imageLoader(view.context).enqueue(request) view.context.imageLoader.enqueue(request)
compactTitle.isVisible = true compactTitle.isVisible = true
gradient.isVisible = true gradient.isVisible = true

View file

@ -5,9 +5,9 @@ import android.graphics.Bitmap
import android.graphics.drawable.BitmapDrawable import android.graphics.drawable.BitmapDrawable
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import coil.Coil import coil3.imageLoader
import coil.request.CachePolicy import coil3.request.CachePolicy
import coil.request.ImageRequest import coil3.request.ImageRequest
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.notification.NotificationHandler import eu.kanade.tachiyomi.data.notification.NotificationHandler
import eu.kanade.tachiyomi.data.notification.NotificationReceiver import eu.kanade.tachiyomi.data.notification.NotificationReceiver
@ -51,7 +51,7 @@ class SaveImageNotifier(private val context: Context) {
} }
}, },
).build() ).build()
Coil.imageLoader(context).enqueue(request) context.imageLoader.enqueue(request)
} }
private fun showCompleteNotification(file: File, image: Bitmap) { private fun showCompleteNotification(file: File, image: Bitmap) {

View file

@ -18,12 +18,14 @@ import androidx.annotation.CallSuper
import androidx.annotation.StyleRes import androidx.annotation.StyleRes
import androidx.appcompat.widget.AppCompatImageView import androidx.appcompat.widget.AppCompatImageView
import androidx.core.view.isVisible import androidx.core.view.isVisible
import coil.dispose import coil3.BitmapImage
import coil.imageLoader import coil3.dispose
import coil.request.CachePolicy import coil3.imageLoader
import coil.request.ImageRequest import coil3.request.CachePolicy
import coil.size.Precision import coil3.request.ImageRequest
import coil.size.ViewSizeResolver import coil3.request.crossfade
import coil3.size.Precision
import coil3.size.ViewSizeResolver
import com.davemorrissey.labs.subscaleview.ImageSource import com.davemorrissey.labs.subscaleview.ImageSource
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView.SCALE_TYPE_CENTER_INSIDE import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView.SCALE_TYPE_CENTER_INSIDE
@ -180,7 +182,7 @@ open class ReaderPageImageView @JvmOverloads constructor(
} }
private fun setNonAnimatedImage( private fun setNonAnimatedImage(
image: Any, data: Any,
config: Config, config: Config,
) = (pageView as? SubsamplingScaleImageView)?.apply { ) = (pageView as? SubsamplingScaleImageView)?.apply {
setDoubleTapZoomDuration(config.zoomDuration.getSystemScaledDuration()) setDoubleTapZoomDuration(config.zoomDuration.getSystemScaledDuration())
@ -226,13 +228,13 @@ open class ReaderPageImageView @JvmOverloads constructor(
val useCoilPipeline = false // FIXME: "Bitmap too large to be uploaded into a texture" val useCoilPipeline = false // FIXME: "Bitmap too large to be uploaded into a texture"
if (isWebtoon && useCoilPipeline) { if (isWebtoon && useCoilPipeline) {
val request = ImageRequest.Builder(context) val request = ImageRequest.Builder(context)
.data(image) .data(data)
.memoryCachePolicy(CachePolicy.DISABLED) .memoryCachePolicy(CachePolicy.DISABLED)
.diskCachePolicy(CachePolicy.DISABLED) .diskCachePolicy(CachePolicy.DISABLED)
.target( .target(
onSuccess = { result -> onSuccess = { result ->
val drawable = result as BitmapDrawable val image = result as BitmapDrawable
setImage(ImageSource.bitmap(drawable.bitmap)) setImage(ImageSource.bitmap(image.bitmap))
isVisible = true isVisible = true
}, },
onError = { onError = {
@ -247,10 +249,10 @@ open class ReaderPageImageView @JvmOverloads constructor(
.build() .build()
context.imageLoader.enqueue(request) context.imageLoader.enqueue(request)
} else { } else {
when (image) { when (data) {
is BitmapDrawable -> setImage(ImageSource.bitmap(image.bitmap)) is BitmapDrawable -> setImage(ImageSource.bitmap(data.bitmap))
is InputStream -> setImage(ImageSource.inputStream(image)) is InputStream -> setImage(ImageSource.inputStream(data))
else -> throw IllegalArgumentException("Not implemented for class ${image::class.simpleName}") else -> throw IllegalArgumentException("Not implemented for class ${data::class.simpleName}")
} }
isVisible = true isVisible = true
} }
@ -314,8 +316,9 @@ open class ReaderPageImageView @JvmOverloads constructor(
.diskCachePolicy(CachePolicy.DISABLED) .diskCachePolicy(CachePolicy.DISABLED)
.target( .target(
onSuccess = { result -> onSuccess = { result ->
setImageDrawable(result) val drawable = result.asDrawable(context.resources)
(result as? Animatable)?.start() setImageDrawable(drawable)
(drawable as? Animatable)?.start()
isVisible = true isVisible = true
this@ReaderPageImageView.onImageLoaded() this@ReaderPageImageView.onImageLoaded()
}, },

View file

@ -5,9 +5,9 @@ import android.view.View
import androidx.core.graphics.ColorUtils import androidx.core.graphics.ColorUtils
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import coil.Coil import coil3.dispose
import coil.dispose import coil3.imageLoader
import coil.request.ImageRequest import coil3.request.ImageRequest
import eu.davidea.flexibleadapter.FlexibleAdapter import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.IFlexible import eu.davidea.flexibleadapter.items.IFlexible
import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.Manga
@ -15,6 +15,7 @@ import eu.kanade.tachiyomi.data.image.coil.CoverViewTarget
import eu.kanade.tachiyomi.data.image.coil.MangaCoverFetcher import eu.kanade.tachiyomi.data.image.coil.MangaCoverFetcher
import eu.kanade.tachiyomi.databinding.MangaGridItemBinding import eu.kanade.tachiyomi.databinding.MangaGridItemBinding
import eu.kanade.tachiyomi.ui.library.LibraryCategoryAdapter import eu.kanade.tachiyomi.ui.library.LibraryCategoryAdapter
import eu.kanade.tachiyomi.util.system.setExtras
import eu.kanade.tachiyomi.util.view.setCards import eu.kanade.tachiyomi.util.view.setCards
/** /**
@ -67,9 +68,9 @@ class BrowseSourceGridHolder(
manga.id ?: return manga.id ?: return
val request = ImageRequest.Builder(view.context).data(manga) val request = ImageRequest.Builder(view.context).data(manga)
.target(CoverViewTarget(binding.coverThumbnail, binding.progress)) .target(CoverViewTarget(binding.coverThumbnail, binding.progress))
.setParameter(MangaCoverFetcher.useCustomCover, false) .setExtras(MangaCoverFetcher.USE_CUSTOM_COVER_KEY, false)
.build() .build()
Coil.imageLoader(view.context).enqueue(request) view.context.imageLoader.enqueue(request)
binding.coverThumbnail.alpha = if (manga.favorite) 0.34f else 1.0f binding.coverThumbnail.alpha = if (manga.favorite) 0.34f else 1.0f
binding.card.strokeColorStateList?.defaultColor?.let { color -> binding.card.strokeColorStateList?.defaultColor?.let { color ->

View file

@ -3,15 +3,16 @@ package eu.kanade.tachiyomi.ui.source.browse
import android.view.View import android.view.View
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import coil.Coil import coil3.dispose
import coil.dispose import coil3.imageLoader
import coil.request.ImageRequest import coil3.request.ImageRequest
import eu.davidea.flexibleadapter.FlexibleAdapter import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.IFlexible import eu.davidea.flexibleadapter.items.IFlexible
import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.image.coil.CoverViewTarget import eu.kanade.tachiyomi.data.image.coil.CoverViewTarget
import eu.kanade.tachiyomi.data.image.coil.MangaCoverFetcher import eu.kanade.tachiyomi.data.image.coil.MangaCoverFetcher
import eu.kanade.tachiyomi.databinding.MangaListItemBinding import eu.kanade.tachiyomi.databinding.MangaListItemBinding
import eu.kanade.tachiyomi.util.system.setExtras
import eu.kanade.tachiyomi.util.view.setCards import eu.kanade.tachiyomi.util.view.setCards
/** /**
@ -56,9 +57,9 @@ class BrowseSourceListHolder(
manga.id ?: return manga.id ?: return
val request = ImageRequest.Builder(view.context).data(manga) val request = ImageRequest.Builder(view.context).data(manga)
.target(CoverViewTarget(binding.coverThumbnail)) .target(CoverViewTarget(binding.coverThumbnail))
.setParameter(MangaCoverFetcher.useCustomCover, false) .setExtras(MangaCoverFetcher.USE_CUSTOM_COVER_KEY, false)
.build() .build()
Coil.imageLoader(view.context).enqueue(request) view.context.imageLoader.enqueue(request)
binding.coverThumbnail.alpha = if (manga.favorite) 0.34f else 1.0f binding.coverThumbnail.alpha = if (manga.favorite) 0.34f else 1.0f
} }

View file

@ -3,16 +3,18 @@ package eu.kanade.tachiyomi.ui.source.globalsearch
import android.graphics.drawable.RippleDrawable import android.graphics.drawable.RippleDrawable
import android.view.View import android.view.View
import androidx.core.view.isVisible import androidx.core.view.isVisible
import coil.Coil import coil3.dispose
import coil.dispose import coil3.imageLoader
import coil.request.CachePolicy import coil3.request.CachePolicy
import coil.request.ImageRequest import coil3.request.ImageRequest
import coil3.request.placeholder
import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.image.coil.CoverViewTarget import eu.kanade.tachiyomi.data.image.coil.CoverViewTarget
import eu.kanade.tachiyomi.data.image.coil.MangaCoverFetcher import eu.kanade.tachiyomi.data.image.coil.MangaCoverFetcher
import eu.kanade.tachiyomi.databinding.SourceGlobalSearchControllerCardItemBinding import eu.kanade.tachiyomi.databinding.SourceGlobalSearchControllerCardItemBinding
import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder
import eu.kanade.tachiyomi.util.system.dpToPx import eu.kanade.tachiyomi.util.system.dpToPx
import eu.kanade.tachiyomi.util.system.setExtras
import eu.kanade.tachiyomi.util.view.makeShapeCorners import eu.kanade.tachiyomi.util.view.makeShapeCorners
import eu.kanade.tachiyomi.util.view.setCards import eu.kanade.tachiyomi.util.view.setCards
@ -57,9 +59,9 @@ class GlobalSearchMangaHolder(view: View, adapter: GlobalSearchCardAdapter) :
.placeholder(android.R.color.transparent) .placeholder(android.R.color.transparent)
.memoryCachePolicy(CachePolicy.DISABLED) .memoryCachePolicy(CachePolicy.DISABLED)
.target(CoverViewTarget(binding.itemImage, binding.progress)) .target(CoverViewTarget(binding.itemImage, binding.progress))
.setParameter(MangaCoverFetcher.useCustomCover, false) .setExtras(MangaCoverFetcher.USE_CUSTOM_COVER_KEY, false)
.build() .build()
Coil.imageLoader(itemView.context).enqueue(request) itemView.context.imageLoader.enqueue(request)
} }
} }
} }

View file

@ -7,8 +7,8 @@ import android.content.pm.ShortcutManager
import android.graphics.Bitmap import android.graphics.Bitmap
import android.graphics.drawable.BitmapDrawable import android.graphics.drawable.BitmapDrawable
import android.graphics.drawable.Icon import android.graphics.drawable.Icon
import coil.Coil import coil3.imageLoader
import coil.request.ImageRequest import coil3.request.ImageRequest
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.appwidget.TachiyomiWidgetManager import eu.kanade.tachiyomi.appwidget.TachiyomiWidgetManager
import eu.kanade.tachiyomi.data.cache.CoverCache import eu.kanade.tachiyomi.data.cache.CoverCache
@ -72,8 +72,8 @@ class MangaShortcutManager(
is Manga -> { is Manga -> {
val request = ImageRequest.Builder(context).data(item).build() val request = ImageRequest.Builder(context).data(item).build()
val bitmap = ( val bitmap = (
Coil.imageLoader(context) context.imageLoader
.execute(request).drawable as? BitmapDrawable .execute(request).image?.asDrawable(context.resources) as? BitmapDrawable
)?.bitmap )?.bitmap
ShortcutInfo.Builder( ShortcutInfo.Builder(

View file

@ -0,0 +1,9 @@
package eu.kanade.tachiyomi.util.system
import coil3.Extras
import coil3.request.ImageRequest
fun <T> ImageRequest.Builder.setExtras(extraKey: Extras.Key<T>, value: T): ImageRequest.Builder {
this.extras[extraKey] = value
return this
}

View file

@ -93,18 +93,20 @@ object ImageUtil {
} }
fun isAnimatedAndSupported(stream: InputStream): Boolean { fun isAnimatedAndSupported(stream: InputStream): Boolean {
try { return try {
val type = getImageType(stream) ?: return false val type = getImageType(stream) ?: return false
return when (type.format) { // https://coil-kt.github.io/coil/getting_started/#supported-image-formats
when (type.format) {
Format.Gif -> true Format.Gif -> true
// Coil supports animated WebP on Android 9.0+ // Animated WebP on Android 9+
// https://coil-kt.github.io/coil/getting_started/#supported-image-formats
Format.Webp -> type.isAnimated && Build.VERSION.SDK_INT >= Build.VERSION_CODES.P Format.Webp -> type.isAnimated && Build.VERSION.SDK_INT >= Build.VERSION_CODES.P
// Animated Heif on Android 11+
Format.Heif -> type.isAnimated && Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
else -> false else -> false
} }
} catch (_: Exception) { } catch (_: Exception) {
false
} }
return false
} }
enum class ImageType(val mime: String, val extension: String) { enum class ImageType(val mime: String, val extension: String) {

View file

@ -1,20 +1,20 @@
package eu.kanade.tachiyomi.widget package eu.kanade.tachiyomi.widget
import android.graphics.drawable.Drawable
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.ImageView import android.widget.ImageView
import androidx.core.view.isVisible import androidx.core.view.isVisible
import coil.target.ImageViewTarget import coil3.Image
import coil3.target.ImageViewTarget
class GifViewTarget(view: ImageView, private val progressBar: View?, private val decodeErrorLayout: ViewGroup?) : ImageViewTarget(view) { class GifViewTarget(view: ImageView, private val progressBar: View?, private val decodeErrorLayout: ViewGroup?) : ImageViewTarget(view) {
override fun onError(error: Drawable?) { override fun onError(error: Image?) {
progressBar?.isVisible = false progressBar?.isVisible = false
decodeErrorLayout?.isVisible = true decodeErrorLayout?.isVisible = true
} }
override fun onSuccess(result: Drawable) { override fun onSuccess(result: Image) {
progressBar?.isVisible = false progressBar?.isVisible = false
decodeErrorLayout?.isVisible = false decodeErrorLayout?.isVisible = false
super.onSuccess(result) super.onSuccess(result)

View file

@ -1,6 +1,6 @@
[versions] [versions]
chucker = "3.5.2" chucker = "3.5.2"
coil = "2.4.0" coil3 = "3.0.0-alpha06"
flexible-adapter = "c8013533" flexible-adapter = "c8013533"
fast_adapter = "5.6.0" fast_adapter = "5.6.0"
nucleus = "3.0.0" nucleus = "3.0.0"
@ -11,9 +11,10 @@ shizuku = "12.1.0"
accompanist-webview = { module = "com.google.accompanist:accompanist-webview", version = "0.30.1" } accompanist-webview = { module = "com.google.accompanist:accompanist-webview", version = "0.30.1" }
chucker-library-no-op = { module = "com.github.ChuckerTeam.Chucker:library-no-op", version.ref = "chucker" } chucker-library-no-op = { module = "com.github.ChuckerTeam.Chucker:library-no-op", version.ref = "chucker" }
chucker-library = { module = "com.github.ChuckerTeam.Chucker:library", version.ref = "chucker" } chucker-library = { module = "com.github.ChuckerTeam.Chucker:library", version.ref = "chucker" }
coil-svg = { module = "io.coil-kt:coil-svg", version.ref = "coil" } coil3 = { module = "io.coil-kt.coil3:coil", version.ref = "coil3" }
coil-gif = { module = "io.coil-kt:coil-gif", version.ref = "coil" } coil3-svg = { module = "io.coil-kt.coil3:coil-svg", version.ref = "coil3" }
coil = { module = "io.coil-kt:coil", version.ref = "coil" } coil3-gif = { module = "io.coil-kt.coil3:coil-gif", version.ref = "coil3" }
coil3-okhttp = { module = "io.coil-kt.coil3:coil-network-okhttp", version.ref = "coil3" }
compose-theme-adapter3 = { module = "com.google.accompanist:accompanist-themeadapter-material3", version = "0.33.2-alpha" } compose-theme-adapter3 = { module = "com.google.accompanist:accompanist-themeadapter-material3", version = "0.33.2-alpha" }
conductor = { module = "com.bluelinelabs:conductor", version = "4.0.0-preview-4" } conductor = { module = "com.bluelinelabs:conductor", version = "4.0.0-preview-4" }
conductor-support-preference = { module = "com.github.tachiyomiorg:conductor-support-preference", version = "3.0.0" } conductor-support-preference = { module = "com.github.tachiyomiorg:conductor-support-preference", version = "3.0.0" }
@ -77,4 +78,5 @@ kotlinter = { id = "org.jmailen.kotlinter", version = "4.1.1" }
gradle-versions = { id = "com.github.ben-manes.versions", version = "0.42.0" } gradle-versions = { id = "com.github.ben-manes.versions", version = "0.42.0" }
[bundles] [bundles]
coil = [ "coil3", "coil3-svg", "coil3-gif", "coil3-okhttp" ]
test = [ "junit", "mockk" ] test = [ "junit", "mockk" ]