Coil 2.x upgrade (#6725)

* Migrate to Coil 2

* Adapt to use coil disk cache

* Update to alpha 7

* Update to alpha 8

* Update to rc01
This commit is contained in:
Ivan Iskandar 2022-03-05 04:04:32 +07:00 committed by GitHub
parent f312936629
commit 10eef282fa
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 286 additions and 204 deletions

View File

@ -20,9 +20,10 @@ import coil.ImageLoader
import coil.ImageLoaderFactory import coil.ImageLoaderFactory
import coil.decode.GifDecoder import coil.decode.GifDecoder
import coil.decode.ImageDecoderDecoder import coil.decode.ImageDecoderDecoder
import coil.disk.DiskCache
import coil.util.DebugLogger import coil.util.DebugLogger
import eu.kanade.tachiyomi.data.coil.ByteBufferFetcher
import eu.kanade.tachiyomi.data.coil.MangaCoverFetcher import eu.kanade.tachiyomi.data.coil.MangaCoverFetcher
import eu.kanade.tachiyomi.data.coil.MangaCoverKeyer
import eu.kanade.tachiyomi.data.coil.TachiyomiImageDecoder import eu.kanade.tachiyomi.data.coil.TachiyomiImageDecoder
import eu.kanade.tachiyomi.data.notification.Notifications import eu.kanade.tachiyomi.data.notification.Notifications
import eu.kanade.tachiyomi.data.preference.PreferenceValues import eu.kanade.tachiyomi.data.preference.PreferenceValues
@ -121,17 +122,20 @@ open class App : Application(), DefaultLifecycleObserver, ImageLoaderFactory {
override fun newImageLoader(): ImageLoader { override fun newImageLoader(): ImageLoader {
return ImageLoader.Builder(this).apply { return ImageLoader.Builder(this).apply {
componentRegistry { val callFactoryInit = { Injekt.get<NetworkHelper>().client }
val diskCacheInit = { CoilDiskCache.get(this@App) }
components {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
add(ImageDecoderDecoder(this@App)) add(ImageDecoderDecoder.Factory())
} else { } else {
add(GifDecoder()) add(GifDecoder.Factory())
} }
add(TachiyomiImageDecoder(this@App.resources)) add(TachiyomiImageDecoder.Factory())
add(ByteBufferFetcher()) add(MangaCoverFetcher.Factory(lazy(callFactoryInit), lazy(diskCacheInit)))
add(MangaCoverFetcher()) add(MangaCoverKeyer())
} }
okHttpClient(Injekt.get<NetworkHelper>().coilClient) callFactory(callFactoryInit)
diskCache(diskCacheInit)
crossfade((300 * this@App.animatorDurationScale).toInt()) crossfade((300 * this@App.animatorDurationScale).toInt())
allowRgb565(getSystemService<ActivityManager>()!!.isLowRamDevice) allowRgb565(getSystemService<ActivityManager>()!!.isLowRamDevice)
if (preferences.verboseLogging()) logger(DebugLogger()) if (preferences.verboseLogging()) logger(DebugLogger())
@ -190,3 +194,24 @@ open class App : Application(), DefaultLifecycleObserver, ImageLoaderFactory {
} }
private const val ACTION_DISABLE_INCOGNITO_MODE = "tachi.action.DISABLE_INCOGNITO_MODE" private const val ACTION_DISABLE_INCOGNITO_MODE = "tachi.action.DISABLE_INCOGNITO_MODE"
/**
* Direct copy of Coil's internal SingletonDiskCache so that [MangaCoverFetcher] can access it.
*/
internal object CoilDiskCache {
private const val FOLDER_NAME = "image_cache"
private var instance: DiskCache? = null
@Synchronized
fun get(context: Context): DiskCache {
return instance ?: run {
val safeCacheDir = context.cacheDir.apply { mkdirs() }
// Create the singleton disk cache instance.
DiskCache.Builder()
.directory(safeCacheDir.resolve(FOLDER_NAME))
.build()
.also { instance = it }
}
}
}

View File

@ -104,7 +104,7 @@ class CoverCache(private val context: Context) {
* Clear coil's memory cache. * Clear coil's memory cache.
*/ */
fun clearMemoryCache() { fun clearMemoryCache() {
context.imageLoader.memoryCache.clear() context.imageLoader.memoryCache?.clear()
} }
private fun getCacheDir(dir: String): File { private fun getCacheDir(dir: String): File {

View File

@ -1,25 +0,0 @@
package eu.kanade.tachiyomi.data.coil
import coil.bitmap.BitmapPool
import coil.decode.DataSource
import coil.decode.Options
import coil.fetch.FetchResult
import coil.fetch.Fetcher
import coil.fetch.SourceResult
import coil.size.Size
import okio.buffer
import okio.source
import java.io.ByteArrayInputStream
import java.nio.ByteBuffer
class ByteBufferFetcher : Fetcher<ByteBuffer> {
override suspend fun fetch(pool: BitmapPool, data: ByteBuffer, size: Size, options: Options): FetchResult {
return SourceResult(
source = ByteArrayInputStream(data.array()).source().buffer(),
mimeType = null,
dataSource = DataSource.MEMORY
)
}
override fun key(data: ByteBuffer): String? = null
}

View File

@ -1,18 +1,18 @@
package eu.kanade.tachiyomi.data.coil package eu.kanade.tachiyomi.data.coil
import coil.bitmap.BitmapPool import coil.ImageLoader
import coil.decode.DataSource import coil.decode.DataSource
import coil.decode.Options import coil.decode.ImageSource
import coil.disk.DiskCache
import coil.fetch.FetchResult import coil.fetch.FetchResult
import coil.fetch.Fetcher import coil.fetch.Fetcher
import coil.fetch.SourceResult import coil.fetch.SourceResult
import coil.network.HttpException import coil.network.HttpException
import coil.request.get import coil.request.Options
import coil.size.Size import coil.request.Parameters
import eu.kanade.tachiyomi.data.cache.CoverCache import eu.kanade.tachiyomi.data.cache.CoverCache
import eu.kanade.tachiyomi.data.coil.MangaCoverFetcher.Companion.USE_CUSTOM_COVER import eu.kanade.tachiyomi.data.coil.MangaCoverFetcher.Companion.USE_CUSTOM_COVER
import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.network.NetworkHelper
import eu.kanade.tachiyomi.network.await import eu.kanade.tachiyomi.network.await
import eu.kanade.tachiyomi.source.SourceManager import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.source.online.HttpSource
@ -20,132 +20,183 @@ import okhttp3.CacheControl
import okhttp3.Call import okhttp3.Call
import okhttp3.Request import okhttp3.Request
import okhttp3.Response import okhttp3.Response
import okhttp3.ResponseBody import okhttp3.internal.closeQuietly
import okio.buffer import okio.Path.Companion.toOkioPath
import okio.sink
import okio.source
import uy.kohesive.injekt.Injekt
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
/** /**
* Coil component that fetches [Manga] cover while using the cached file in disk when available. * A [Fetcher] that fetches cover image for [Manga] object.
*
* It uses [Manga.thumbnail_url] if custom cover is not set by the user.
* Disk caching for library items is handled by [CoverCache], otherwise
* handled by Coil's [DiskCache].
* *
* Available request parameter: * Available request parameter:
* - [USE_CUSTOM_COVER]: Use custom cover if set by user, default is true * - [USE_CUSTOM_COVER]: Use custom cover if set by user, default is true
*/ */
class MangaCoverFetcher : Fetcher<Manga> { class MangaCoverFetcher(
private val coverCache: CoverCache by injectLazy() private val manga: Manga,
private val sourceManager: SourceManager by injectLazy() private val sourceLazy: Lazy<HttpSource?>,
private val defaultClient = Injekt.get<NetworkHelper>().coilClient private val options: Options,
private val coverCache: CoverCache,
private val callFactoryLazy: Lazy<Call.Factory>,
private val diskCacheLazy: Lazy<DiskCache>
) : Fetcher {
override fun key(data: Manga): String? { // For non-custom cover
if (data.thumbnail_url.isNullOrBlank()) return null private val diskCacheKey: String? by lazy { MangaCoverKeyer().key(manga, options) }
return data.thumbnail_url!! private lateinit var url: String
}
override suspend fun fetch(pool: BitmapPool, data: Manga, size: Size, options: Options): FetchResult { override suspend fun fetch(): FetchResult {
// Use custom cover if exists // Use custom cover if exists
val useCustomCover = options.parameters[USE_CUSTOM_COVER] as? Boolean ?: true val useCustomCover = options.parameters.value(USE_CUSTOM_COVER) ?: true
val customCoverFile = coverCache.getCustomCoverFile(data) val customCoverFile = coverCache.getCustomCoverFile(manga)
if (useCustomCover && customCoverFile.exists()) { if (useCustomCover && customCoverFile.exists()) {
return fileLoader(customCoverFile) return fileLoader(customCoverFile)
} }
val cover = data.thumbnail_url // diskCacheKey is thumbnail_url
return when (getResourceType(cover)) { url = diskCacheKey ?: error("No cover specified")
Type.URL -> httpLoader(data, options) return when (getResourceType(url)) {
Type.File -> fileLoader(data) Type.URL -> httpLoader()
Type.File -> fileLoader(File(url.substringAfter("file://")))
null -> error("Invalid image") null -> error("Invalid image")
} }
} }
private suspend fun httpLoader(manga: Manga, options: Options): FetchResult { private fun fileLoader(file: File): FetchResult {
return SourceResult(
source = ImageSource(file = file.toOkioPath(), diskCacheKey = diskCacheKey),
mimeType = "image/*",
dataSource = DataSource.DISK
)
}
private suspend fun httpLoader(): FetchResult {
// Only cache separately if it's a library item // Only cache separately if it's a library item
val coverCacheFile = if (manga.favorite) { val coverCacheFile = if (manga.favorite) {
coverCache.getCoverFile(manga) ?: error("No cover specified") coverCache.getCoverFile(manga) ?: error("No cover specified")
} else { } else {
null null
} }
if (coverCacheFile?.exists() == true && options.diskCachePolicy.readEnabled) { if (coverCacheFile?.exists() == true && options.diskCachePolicy.readEnabled) {
return fileLoader(coverCacheFile) return fileLoader(coverCacheFile)
} }
val (response, body) = awaitGetCall(manga, options) var snapshot = readFromDiskCache()
if (!response.isSuccessful) { try {
body.close() // Fetch from disk cache
throw HttpException(response) if (snapshot != null) {
}
if (coverCacheFile != null && options.diskCachePolicy.writeEnabled) {
@Suppress("BlockingMethodInNonBlockingContext")
response.peekBody(Long.MAX_VALUE).source().use { input ->
coverCacheFile.parentFile?.mkdirs()
if (coverCacheFile.exists()) {
coverCacheFile.delete()
}
coverCacheFile.sink().buffer().use { output ->
output.writeAll(input)
}
}
}
return SourceResult( return SourceResult(
source = body.source(), source = snapshot.toImageSource(),
mimeType = "image/*",
dataSource = if (response.cacheResponse != null) DataSource.DISK else DataSource.NETWORK
)
}
private suspend fun awaitGetCall(manga: Manga, options: Options): Pair<Response, ResponseBody> {
val call = getCall(manga, options)
val response = call.await()
return response to checkNotNull(response.body) { "Null response source" }
}
private fun getCall(manga: Manga, options: Options): Call {
val source = sourceManager.get(manga.source) as? HttpSource
val request = Request.Builder().url(manga.thumbnail_url!!).also {
if (source != null) {
it.headers(source.headers)
}
val networkRead = options.networkCachePolicy.readEnabled
val diskRead = options.diskCachePolicy.readEnabled
when {
!networkRead && diskRead -> {
it.cacheControl(CacheControl.FORCE_CACHE)
}
networkRead && !diskRead -> if (options.diskCachePolicy.writeEnabled) {
it.cacheControl(CacheControl.FORCE_NETWORK)
} else {
it.cacheControl(CACHE_CONTROL_FORCE_NETWORK_NO_CACHE)
}
!networkRead && !diskRead -> {
// This causes the request to fail with a 504 Unsatisfiable Request.
it.cacheControl(CACHE_CONTROL_NO_NETWORK_NO_CACHE)
}
}
}.build()
val client = source?.client?.newBuilder()?.cache(defaultClient.cache)?.build() ?: defaultClient
return client.newCall(request)
}
private fun fileLoader(manga: Manga): FetchResult {
return fileLoader(File(manga.thumbnail_url!!.substringAfter("file://")))
}
private fun fileLoader(file: File): FetchResult {
return SourceResult(
source = file.source().buffer(),
mimeType = "image/*", mimeType = "image/*",
dataSource = DataSource.DISK dataSource = DataSource.DISK
) )
} }
// Fetch from network
val response = executeNetworkRequest()
val responseBody = checkNotNull(response.body) { "Null response source" }
try {
snapshot = writeToDiskCache(snapshot, response)
// Read from disk cache
if (snapshot != null) {
return SourceResult(
source = snapshot.toImageSource(),
mimeType = "image/*",
dataSource = DataSource.NETWORK
)
}
// Read from response if cache is unused or unusable
return SourceResult(
source = ImageSource(source = responseBody.source(), context = options.context),
mimeType = "image/*",
dataSource = if (response.cacheResponse != null) DataSource.DISK else DataSource.NETWORK
)
} catch (e: Exception) {
responseBody.closeQuietly()
throw e
} finally {
response.close()
}
} catch (e: Exception) {
snapshot?.closeQuietly()
throw e
}
}
private suspend fun executeNetworkRequest(): Response {
val client = sourceLazy.value?.client ?: callFactoryLazy.value
val response = client.newCall(newRequest()).await()
if (!response.isSuccessful && response.code != HttpURLConnection.HTTP_NOT_MODIFIED) {
response.body?.closeQuietly()
throw HttpException(response)
}
return response
}
private fun newRequest(): Request {
val request = Request.Builder()
.url(url)
.headers(options.headers)
// Support attaching custom data to the network request.
.tag(Parameters::class.java, options.parameters)
val diskRead = options.diskCachePolicy.readEnabled
val networkRead = options.networkCachePolicy.readEnabled
when {
!networkRead && diskRead -> {
request.cacheControl(CacheControl.FORCE_CACHE)
}
networkRead && !diskRead -> if (options.diskCachePolicy.writeEnabled) {
request.cacheControl(CacheControl.FORCE_NETWORK)
} else {
request.cacheControl(CACHE_CONTROL_FORCE_NETWORK_NO_CACHE)
}
!networkRead && !diskRead -> {
// This causes the request to fail with a 504 Unsatisfiable Request.
request.cacheControl(CACHE_CONTROL_NO_NETWORK_NO_CACHE)
}
}
return request.build()
}
private fun readFromDiskCache(): DiskCache.Snapshot? {
return if (options.diskCachePolicy.readEnabled) diskCacheLazy.value[diskCacheKey!!] else null
}
private fun writeToDiskCache(snapshot: DiskCache.Snapshot?, response: Response): DiskCache.Snapshot? {
if (!options.diskCachePolicy.writeEnabled) {
snapshot?.closeQuietly()
return null
}
val editor = if (snapshot != null) {
snapshot.closeAndEdit()
} else {
diskCacheLazy.value.edit(diskCacheKey!!)
} ?: return null
try {
diskCacheLazy.value.fileSystem.write(editor.data) {
response.body!!.source().readAll(this)
}
return editor.commitAndGet()
} catch (e: Exception) {
try {
editor.abort()
} catch (ignored: Exception) {
}
throw e
}
}
private fun DiskCache.Snapshot.toImageSource(): ImageSource {
return ImageSource(file = data, diskCacheKey = diskCacheKey, closeable = this)
}
private fun getResourceType(cover: String?): Type? { private fun getResourceType(cover: String?): Type? {
return when { return when {
cover.isNullOrEmpty() -> null cover.isNullOrEmpty() -> null
@ -159,6 +210,20 @@ class MangaCoverFetcher : Fetcher<Manga> {
File, URL File, URL
} }
class Factory(
private val callFactoryLazy: Lazy<Call.Factory>,
private val diskCacheLazy: Lazy<DiskCache>
) : Fetcher.Factory<Manga> {
private val coverCache: CoverCache by injectLazy()
private val sourceManager: SourceManager by injectLazy()
override fun create(data: Manga, options: Options, imageLoader: ImageLoader): Fetcher {
val source = lazy { sourceManager.get(data.source) as? HttpSource }
return MangaCoverFetcher(data, source, options, coverCache, callFactoryLazy, diskCacheLazy)
}
}
companion object { companion object {
const val USE_CUSTOM_COVER = "use_custom_cover" const val USE_CUSTOM_COVER = "use_custom_cover"

View File

@ -0,0 +1,11 @@
package eu.kanade.tachiyomi.data.coil
import coil.key.Keyer
import coil.request.Options
import eu.kanade.tachiyomi.data.database.models.Manga
class MangaCoverKeyer : Keyer<Manga> {
override fun key(data: Manga, options: Options): String? {
return data.thumbnail_url?.takeIf { it.isNotBlank() }
}
}

View File

@ -1,13 +1,14 @@
package eu.kanade.tachiyomi.data.coil package eu.kanade.tachiyomi.data.coil
import android.content.res.Resources
import android.os.Build import android.os.Build
import androidx.core.graphics.drawable.toDrawable import androidx.core.graphics.drawable.toDrawable
import coil.bitmap.BitmapPool import coil.ImageLoader
import coil.decode.DecodeResult import coil.decode.DecodeResult
import coil.decode.Decoder import coil.decode.Decoder
import coil.decode.Options import coil.decode.ImageDecoderDecoder
import coil.size.Size import coil.decode.ImageSource
import coil.fetch.SourceResult
import coil.request.Options
import eu.kanade.tachiyomi.util.system.ImageUtil import eu.kanade.tachiyomi.util.system.ImageUtil
import okio.BufferedSource import okio.BufferedSource
import tachiyomi.decoder.ImageDecoder import tachiyomi.decoder.ImageDecoder
@ -15,26 +16,10 @@ import tachiyomi.decoder.ImageDecoder
/** /**
* A [Decoder] that uses built-in [ImageDecoder] to decode images that is not supported by the system. * A [Decoder] that uses built-in [ImageDecoder] to decode images that is not supported by the system.
*/ */
class TachiyomiImageDecoder(private val resources: Resources) : Decoder { class TachiyomiImageDecoder(private val resources: ImageSource, private val options: Options) : Decoder {
override fun handles(source: BufferedSource, mimeType: String?): Boolean { override suspend fun decode(): DecodeResult {
val type = source.peek().inputStream().use { val decoder = resources.sourceOrNull()?.use {
ImageUtil.findImageType(it)
}
return when (type) {
ImageUtil.ImageType.AVIF, ImageUtil.ImageType.JXL -> true
ImageUtil.ImageType.HEIF -> Build.VERSION.SDK_INT < Build.VERSION_CODES.O
else -> false
}
}
override suspend fun decode(
pool: BitmapPool,
source: BufferedSource,
size: Size,
options: Options
): DecodeResult {
val decoder = source.use {
ImageDecoder.newInstance(it.inputStream()) ImageDecoder.newInstance(it.inputStream())
} }
@ -46,8 +31,31 @@ class TachiyomiImageDecoder(private val resources: Resources) : Decoder {
check(bitmap != null) { "Failed to decode image." } check(bitmap != null) { "Failed to decode image." }
return DecodeResult( return DecodeResult(
drawable = bitmap.toDrawable(resources), drawable = bitmap.toDrawable(options.context.resources),
isSampled = false isSampled = false
) )
} }
class Factory : Decoder.Factory {
override fun create(result: SourceResult, options: Options, imageLoader: ImageLoader): Decoder? {
if (!isApplicable(result.source.source())) return null
return TachiyomiImageDecoder(result.source, options)
}
private fun isApplicable(source: BufferedSource): Boolean {
val type = source.peek().inputStream().use {
ImageUtil.findImageType(it)
}
return when (type) {
ImageUtil.ImageType.AVIF, ImageUtil.ImageType.JXL -> true
ImageUtil.ImageType.HEIF -> Build.VERSION.SDK_INT < Build.VERSION_CODES.O
else -> false
}
}
override fun equals(other: Any?) = other is ImageDecoderDecoder.Factory
override fun hashCode() = javaClass.hashCode()
}
} }

View File

@ -1,7 +1,6 @@
package eu.kanade.tachiyomi.network package eu.kanade.tachiyomi.network
import android.content.Context import android.content.Context
import coil.util.CoilUtils
import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.network.interceptor.CloudflareInterceptor import eu.kanade.tachiyomi.network.interceptor.CloudflareInterceptor
import eu.kanade.tachiyomi.network.interceptor.UserAgentInterceptor import eu.kanade.tachiyomi.network.interceptor.UserAgentInterceptor
@ -49,8 +48,6 @@ class NetworkHelper(context: Context) {
val client by lazy { baseClientBuilder.cache(Cache(cacheDir, cacheSize)).build() } val client by lazy { baseClientBuilder.cache(Cache(cacheDir, cacheSize)).build() }
val coilClient by lazy { baseClientBuilder.cache(CoilUtils.createDefaultCache(context)).build() }
val cloudflareClient by lazy { val cloudflareClient by lazy {
client.newBuilder() client.newBuilder()
.addInterceptor(CloudflareInterceptor(context)) .addInterceptor(CloudflareInterceptor(context))

View File

@ -2,7 +2,7 @@ package eu.kanade.tachiyomi.ui.browse.extension
import android.view.View import android.view.View
import androidx.core.view.isVisible import androidx.core.view.isVisible
import coil.clear import coil.dispose
import coil.load import coil.load
import eu.davidea.viewholders.FlexibleViewHolder import eu.davidea.viewholders.FlexibleViewHolder
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
@ -39,7 +39,7 @@ class ExtensionHolder(view: View, val adapter: ExtensionAdapter) :
else -> "" else -> ""
}.uppercase() }.uppercase()
binding.icon.clear() binding.icon.dispose()
if (extension is Extension.Available) { if (extension is Extension.Available) {
binding.icon.load(extension.iconUrl) binding.icon.load(extension.iconUrl)
} else if (extension is Extension.Installed) { } else if (extension is Extension.Installed) {

View File

@ -1,8 +1,8 @@
package eu.kanade.tachiyomi.ui.browse.migration.manga package eu.kanade.tachiyomi.ui.browse.migration.manga
import android.view.View import android.view.View
import coil.clear import coil.dispose
import coil.loadAny import coil.load
import eu.davidea.viewholders.FlexibleViewHolder import eu.davidea.viewholders.FlexibleViewHolder
import eu.kanade.tachiyomi.databinding.SourceListItemBinding import eu.kanade.tachiyomi.databinding.SourceListItemBinding
@ -23,7 +23,7 @@ class MigrationMangaHolder(
binding.title.text = item.manga.title binding.title.text = item.manga.title
// Update the cover // Update the cover
binding.thumbnail.clear() binding.thumbnail.dispose()
binding.thumbnail.loadAny(item.manga) binding.thumbnail.load(item.manga)
} }
} }

View File

@ -1,7 +1,7 @@
package eu.kanade.tachiyomi.ui.browse.source.browse package eu.kanade.tachiyomi.ui.browse.source.browse
import androidx.core.view.isVisible import androidx.core.view.isVisible
import coil.clear import coil.dispose
import coil.imageLoader import coil.imageLoader
import coil.request.ImageRequest import coil.request.ImageRequest
import coil.transition.CrossfadeTransition import coil.transition.CrossfadeTransition
@ -48,10 +48,10 @@ class SourceComfortableGridHolder(
} }
override fun setImage(manga: Manga) { override fun setImage(manga: Manga) {
binding.thumbnail.clear() binding.thumbnail.dispose()
if (!manga.thumbnail_url.isNullOrEmpty()) { if (!manga.thumbnail_url.isNullOrEmpty()) {
val crossfadeDuration = binding.root.context.imageLoader.defaults.transition.let { val crossfadeDuration = binding.root.context.imageLoader.defaults.transitionFactory.let {
if (it is CrossfadeTransition) it.durationMillis else 0 if (it is CrossfadeTransition.Factory) it.durationMillis else 0
} }
val request = ImageRequest.Builder(binding.root.context) val request = ImageRequest.Builder(binding.root.context)
.data(manga) .data(manga)

View File

@ -1,7 +1,7 @@
package eu.kanade.tachiyomi.ui.browse.source.browse package eu.kanade.tachiyomi.ui.browse.source.browse
import androidx.core.view.isVisible import androidx.core.view.isVisible
import coil.clear import coil.dispose
import coil.imageLoader import coil.imageLoader
import coil.request.ImageRequest import coil.request.ImageRequest
import coil.transition.CrossfadeTransition import coil.transition.CrossfadeTransition
@ -48,10 +48,10 @@ class SourceCompactGridHolder(
} }
override fun setImage(manga: Manga) { override fun setImage(manga: Manga) {
binding.thumbnail.clear() binding.thumbnail.dispose()
if (!manga.thumbnail_url.isNullOrEmpty()) { if (!manga.thumbnail_url.isNullOrEmpty()) {
val crossfadeDuration = binding.root.context.imageLoader.defaults.transition.let { val crossfadeDuration = binding.root.context.imageLoader.defaults.transitionFactory.let {
if (it is CrossfadeTransition) it.durationMillis else 0 if (it is CrossfadeTransition.Factory) it.durationMillis else 0
} }
val request = ImageRequest.Builder(binding.root.context) val request = ImageRequest.Builder(binding.root.context)
.data(manga) .data(manga)

View File

@ -2,8 +2,8 @@ package eu.kanade.tachiyomi.ui.browse.source.browse
import android.view.View import android.view.View
import androidx.core.view.isVisible import androidx.core.view.isVisible
import coil.clear import coil.dispose
import coil.loadAny import coil.load
import eu.davidea.flexibleadapter.FlexibleAdapter import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.coil.MangaCoverFetcher import eu.kanade.tachiyomi.data.coil.MangaCoverFetcher
@ -50,9 +50,9 @@ class SourceListHolder(private val view: View, adapter: FlexibleAdapter<*>) :
} }
override fun setImage(manga: Manga) { override fun setImage(manga: Manga) {
binding.thumbnail.clear() binding.thumbnail.dispose()
if (!manga.thumbnail_url.isNullOrEmpty()) { if (!manga.thumbnail_url.isNullOrEmpty()) {
binding.thumbnail.loadAny(manga) { binding.thumbnail.load(manga) {
setParameter(MangaCoverFetcher.USE_CUSTOM_COVER, false) setParameter(MangaCoverFetcher.USE_CUSTOM_COVER, false)
} }
} }

View File

@ -2,7 +2,7 @@ package eu.kanade.tachiyomi.ui.browse.source.globalsearch
import android.view.View import android.view.View
import androidx.core.view.isVisible import androidx.core.view.isVisible
import coil.clear import coil.dispose
import coil.imageLoader import coil.imageLoader
import coil.request.ImageRequest import coil.request.ImageRequest
import coil.transition.CrossfadeTransition import coil.transition.CrossfadeTransition
@ -53,10 +53,10 @@ class GlobalSearchCardHolder(view: View, adapter: GlobalSearchCardAdapter) :
} }
fun setImage(manga: Manga) { fun setImage(manga: Manga) {
binding.cover.clear() binding.cover.dispose()
if (!manga.thumbnail_url.isNullOrEmpty()) { if (!manga.thumbnail_url.isNullOrEmpty()) {
val crossfadeDuration = itemView.context.imageLoader.defaults.transition.let { val crossfadeDuration = itemView.context.imageLoader.defaults.transitionFactory.let {
if (it is CrossfadeTransition) it.durationMillis else 0 if (it is CrossfadeTransition.Factory) it.durationMillis else 0
} }
val request = ImageRequest.Builder(itemView.context) val request = ImageRequest.Builder(itemView.context)
.data(manga) .data(manga)

View File

@ -2,11 +2,11 @@ package eu.kanade.tachiyomi.ui.library
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import coil.clear import coil.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.databinding.SourceComfortableGridItemBinding import eu.kanade.tachiyomi.databinding.SourceComfortableGridItemBinding
import eu.kanade.tachiyomi.util.view.loadAnyAutoPause import eu.kanade.tachiyomi.util.view.loadAutoPause
/** /**
* Class used to hold the displayed data of a manga in the library, like the cover or the title. * Class used to hold the displayed data of a manga in the library, like the cover or the title.
@ -55,7 +55,7 @@ class LibraryComfortableGridHolder(
binding.badges.localText.isVisible = item.isLocal binding.badges.localText.isVisible = item.isLocal
// Update the cover. // Update the cover.
binding.thumbnail.clear() binding.thumbnail.dispose()
binding.thumbnail.loadAnyAutoPause(item.manga) binding.thumbnail.loadAutoPause(item.manga)
} }
} }

View File

@ -1,10 +1,10 @@
package eu.kanade.tachiyomi.ui.library package eu.kanade.tachiyomi.ui.library
import androidx.core.view.isVisible import androidx.core.view.isVisible
import coil.clear import coil.dispose
import eu.davidea.flexibleadapter.FlexibleAdapter import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.kanade.tachiyomi.databinding.SourceCompactGridItemBinding import eu.kanade.tachiyomi.databinding.SourceCompactGridItemBinding
import eu.kanade.tachiyomi.util.view.loadAnyAutoPause import eu.kanade.tachiyomi.util.view.loadAutoPause
/** /**
* Class used to hold the displayed data of a manga in the library, like the cover or the title. * Class used to hold the displayed data of a manga in the library, like the cover or the title.
@ -54,11 +54,11 @@ class LibraryCompactGridHolder(
binding.badges.localText.isVisible = item.isLocal binding.badges.localText.isVisible = item.isLocal
// Update the cover. // Update the cover.
binding.thumbnail.clear() binding.thumbnail.dispose()
if (coverOnly) { if (coverOnly) {
// Cover only mode: Hides title text unless thumbnail is unavailable // Cover only mode: Hides title text unless thumbnail is unavailable
if (!item.manga.thumbnail_url.isNullOrEmpty()) { if (!item.manga.thumbnail_url.isNullOrEmpty()) {
binding.thumbnail.loadAnyAutoPause(item.manga) binding.thumbnail.loadAutoPause(item.manga)
binding.title.isVisible = false binding.title.isVisible = false
} else { } else {
binding.title.text = item.manga.title binding.title.text = item.manga.title
@ -66,7 +66,7 @@ class LibraryCompactGridHolder(
} }
binding.thumbnail.foreground = null binding.thumbnail.foreground = null
} else { } else {
binding.thumbnail.loadAnyAutoPause(item.manga) binding.thumbnail.loadAutoPause(item.manga)
} }
} }
} }

View File

@ -2,8 +2,8 @@ package eu.kanade.tachiyomi.ui.library
import android.view.View import android.view.View
import androidx.core.view.isVisible import androidx.core.view.isVisible
import coil.clear import coil.dispose
import coil.loadAny import coil.load
import eu.davidea.flexibleadapter.FlexibleAdapter import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.kanade.tachiyomi.databinding.SourceListItemBinding import eu.kanade.tachiyomi.databinding.SourceListItemBinding
@ -61,7 +61,7 @@ class LibraryListHolder(
} }
// Update the cover // Update the cover
binding.thumbnail.clear() binding.thumbnail.dispose()
binding.thumbnail.loadAny(item.manga) binding.thumbnail.load(item.manga)
} }
} }

View File

@ -334,7 +334,7 @@ class MangaPresenter(
* @return cover as Bitmap or null if there is no thumbnail cached with the memoryCacheKey * @return cover as Bitmap or null if there is no thumbnail cached with the memoryCacheKey
*/ */
private fun coverBitmapFromImageLoader(context: Context, memoryCacheKey: MemoryCache.Key): Bitmap? { private fun coverBitmapFromImageLoader(context: Context, memoryCacheKey: MemoryCache.Key): Bitmap? {
return context.imageLoader.memoryCache[memoryCacheKey] return context.imageLoader.memoryCache?.get(memoryCacheKey)?.bitmap
} }
/** /**

View File

@ -19,7 +19,7 @@ import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.ui.base.controller.getMainAppBarHeight import eu.kanade.tachiyomi.ui.base.controller.getMainAppBarHeight
import eu.kanade.tachiyomi.ui.manga.MangaController import eu.kanade.tachiyomi.ui.manga.MangaController
import eu.kanade.tachiyomi.util.system.copyToClipboard import eu.kanade.tachiyomi.util.system.copyToClipboard
import eu.kanade.tachiyomi.util.view.loadAnyAutoPause import eu.kanade.tachiyomi.util.view.loadAutoPause
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import reactivecircus.flowbinding.android.view.clicks import reactivecircus.flowbinding.android.view.clicks
@ -286,8 +286,8 @@ class MangaInfoHeaderAdapter(
setFavoriteButtonState(manga.favorite) setFavoriteButtonState(manga.favorite)
// Set cover if changed. // Set cover if changed.
binding.backdrop.loadAnyAutoPause(manga) binding.backdrop.loadAutoPause(manga)
binding.mangaCover.loadAnyAutoPause(manga) binding.mangaCover.loadAutoPause(manga)
// Manga info section // Manga info section
binding.mangaSummarySection.setTags(manga.getGenres(), controller::performGenreSearch) binding.mangaSummarySection.setTags(manga.getGenres(), controller::performGenreSearch)

View File

@ -2,8 +2,8 @@ package eu.kanade.tachiyomi.ui.manga.track
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import coil.clear import coil.dispose
import coil.loadAny import coil.load
import eu.kanade.tachiyomi.data.track.model.TrackSearch import eu.kanade.tachiyomi.data.track.model.TrackSearch
import eu.kanade.tachiyomi.databinding.TrackSearchItemBinding import eu.kanade.tachiyomi.databinding.TrackSearchItemBinding
import java.util.Locale import java.util.Locale
@ -20,9 +20,9 @@ class TrackSearchHolder(
} }
binding.trackSearchTitle.text = track.title binding.trackSearchTitle.text = track.title
binding.trackSearchCover.clear() binding.trackSearchCover.dispose()
if (track.cover_url.isNotEmpty()) { if (track.cover_url.isNotEmpty()) {
binding.trackSearchCover.loadAny(track.cover_url) binding.trackSearchCover.load(track.cover_url)
} }
val hasStatus = track.publishing_status.isNotBlank() val hasStatus = track.publishing_status.isNotBlank()

View File

@ -17,7 +17,7 @@ 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.clear import coil.dispose
import coil.imageLoader import coil.imageLoader
import coil.request.CachePolicy import coil.request.CachePolicy
import coil.request.ImageRequest import coil.request.ImageRequest
@ -152,7 +152,7 @@ open class ReaderPageImageView @JvmOverloads constructor(
fun recycle() = pageView?.let { fun recycle() = pageView?.let {
when (it) { when (it) {
is SubsamplingScaleImageView -> it.recycle() is SubsamplingScaleImageView -> it.recycle()
is AppCompatImageView -> it.clear() is AppCompatImageView -> it.dispose()
} }
it.isVisible = false it.isVisible = false
} }

View File

@ -1,8 +1,8 @@
package eu.kanade.tachiyomi.ui.recent.history package eu.kanade.tachiyomi.ui.recent.history
import android.view.View import android.view.View
import coil.clear import coil.dispose
import coil.loadAny import coil.load
import eu.davidea.viewholders.FlexibleViewHolder import eu.davidea.viewholders.FlexibleViewHolder
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.MangaChapterHistory import eu.kanade.tachiyomi.data.database.models.MangaChapterHistory
@ -65,7 +65,7 @@ class HistoryHolder(
} }
// Set cover // Set cover
binding.cover.clear() binding.cover.dispose()
binding.cover.loadAny(item.manga) binding.cover.load(item.manga)
} }
} }

View File

@ -2,8 +2,8 @@ package eu.kanade.tachiyomi.ui.recent.updates
import android.view.View import android.view.View
import androidx.core.view.isVisible import androidx.core.view.isVisible
import coil.clear import coil.dispose
import coil.loadAny import coil.load
import eu.kanade.tachiyomi.databinding.UpdatesItemBinding import eu.kanade.tachiyomi.databinding.UpdatesItemBinding
import eu.kanade.tachiyomi.source.LocalSource import eu.kanade.tachiyomi.source.LocalSource
import eu.kanade.tachiyomi.ui.manga.chapter.base.BaseChapterHolder import eu.kanade.tachiyomi.ui.manga.chapter.base.BaseChapterHolder
@ -58,7 +58,7 @@ class UpdatesHolder(private val view: View, private val adapter: UpdatesAdapter)
binding.download.setState(item.status, item.progress) binding.download.setState(item.status, item.progress)
// Set cover // Set cover
binding.mangaCover.clear() binding.mangaCover.dispose()
binding.mangaCover.loadAny(item.manga) binding.mangaCover.load(item.manga)
} }
} }

View File

@ -8,7 +8,7 @@ import androidx.annotation.DrawableRes
import androidx.appcompat.content.res.AppCompatResources import androidx.appcompat.content.res.AppCompatResources
import coil.ImageLoader import coil.ImageLoader
import coil.imageLoader import coil.imageLoader
import coil.loadAny import coil.load
import coil.request.ImageRequest import coil.request.ImageRequest
import coil.target.ImageViewTarget import coil.target.ImageViewTarget
import eu.kanade.tachiyomi.util.system.animatorDurationScale import eu.kanade.tachiyomi.util.system.animatorDurationScale
@ -33,12 +33,13 @@ fun ImageView.setVectorCompat(@DrawableRes drawable: Int, @AttrRes tint: Int? =
* and if the image is animated, this will also disable that animation * and if the image is animated, this will also disable that animation
* if [Context.animatorDurationScale] is 0 * if [Context.animatorDurationScale] is 0
*/ */
fun ImageView.loadAnyAutoPause( fun ImageView.loadAutoPause(
data: Any?, data: Any?,
loader: ImageLoader = context.imageLoader, loader: ImageLoader = context.imageLoader,
builder: ImageRequest.Builder.() -> Unit = {} builder: ImageRequest.Builder.() -> Unit = {}
) { ) {
this.loadAny(data, loader) { // Build the original request so we can add on our success listener
load(data, loader) {
// Build the original request so we can add on our success listener // Build the original request so we can add on our success listener
val originalBuild = apply(builder).build() val originalBuild = apply(builder).build()
listener( listener(

View File

@ -2,7 +2,7 @@
aboutlib_version = "8.9.4" aboutlib_version = "8.9.4"
okhttp_version = "4.9.1" okhttp_version = "4.9.1"
nucleus_version = "3.0.0" nucleus_version = "3.0.0"
coil_version = "1.4.0" coil_version = "2.0.0-rc01"
conductor_version = "3.1.2" conductor_version = "3.1.2"
flowbinding_version = "1.2.0" flowbinding_version = "1.2.0"
shizuku_version = "12.1.0" shizuku_version = "12.1.0"