mirror of
				https://github.com/mihonapp/mihon.git
				synced 2025-11-04 08:08:55 +01:00 
			
		
		
		
	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:
		@@ -20,9 +20,10 @@ import coil.ImageLoader
 | 
			
		||||
import coil.ImageLoaderFactory
 | 
			
		||||
import coil.decode.GifDecoder
 | 
			
		||||
import coil.decode.ImageDecoderDecoder
 | 
			
		||||
import coil.disk.DiskCache
 | 
			
		||||
import coil.util.DebugLogger
 | 
			
		||||
import eu.kanade.tachiyomi.data.coil.ByteBufferFetcher
 | 
			
		||||
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.notification.Notifications
 | 
			
		||||
import eu.kanade.tachiyomi.data.preference.PreferenceValues
 | 
			
		||||
@@ -121,17 +122,20 @@ open class App : Application(), DefaultLifecycleObserver, ImageLoaderFactory {
 | 
			
		||||
 | 
			
		||||
    override fun newImageLoader(): ImageLoader {
 | 
			
		||||
        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) {
 | 
			
		||||
                    add(ImageDecoderDecoder(this@App))
 | 
			
		||||
                    add(ImageDecoderDecoder.Factory())
 | 
			
		||||
                } else {
 | 
			
		||||
                    add(GifDecoder())
 | 
			
		||||
                    add(GifDecoder.Factory())
 | 
			
		||||
                }
 | 
			
		||||
                add(TachiyomiImageDecoder(this@App.resources))
 | 
			
		||||
                add(ByteBufferFetcher())
 | 
			
		||||
                add(MangaCoverFetcher())
 | 
			
		||||
                add(TachiyomiImageDecoder.Factory())
 | 
			
		||||
                add(MangaCoverFetcher.Factory(lazy(callFactoryInit), lazy(diskCacheInit)))
 | 
			
		||||
                add(MangaCoverKeyer())
 | 
			
		||||
            }
 | 
			
		||||
            okHttpClient(Injekt.get<NetworkHelper>().coilClient)
 | 
			
		||||
            callFactory(callFactoryInit)
 | 
			
		||||
            diskCache(diskCacheInit)
 | 
			
		||||
            crossfade((300 * this@App.animatorDurationScale).toInt())
 | 
			
		||||
            allowRgb565(getSystemService<ActivityManager>()!!.isLowRamDevice)
 | 
			
		||||
            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"
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * 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 }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -104,7 +104,7 @@ class CoverCache(private val context: Context) {
 | 
			
		||||
     * Clear coil's memory cache.
 | 
			
		||||
     */
 | 
			
		||||
    fun clearMemoryCache() {
 | 
			
		||||
        context.imageLoader.memoryCache.clear()
 | 
			
		||||
        context.imageLoader.memoryCache?.clear()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun getCacheDir(dir: String): 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
 | 
			
		||||
}
 | 
			
		||||
@@ -1,18 +1,18 @@
 | 
			
		||||
package eu.kanade.tachiyomi.data.coil
 | 
			
		||||
 | 
			
		||||
import coil.bitmap.BitmapPool
 | 
			
		||||
import coil.ImageLoader
 | 
			
		||||
import coil.decode.DataSource
 | 
			
		||||
import coil.decode.Options
 | 
			
		||||
import coil.decode.ImageSource
 | 
			
		||||
import coil.disk.DiskCache
 | 
			
		||||
import coil.fetch.FetchResult
 | 
			
		||||
import coil.fetch.Fetcher
 | 
			
		||||
import coil.fetch.SourceResult
 | 
			
		||||
import coil.network.HttpException
 | 
			
		||||
import coil.request.get
 | 
			
		||||
import coil.size.Size
 | 
			
		||||
import coil.request.Options
 | 
			
		||||
import coil.request.Parameters
 | 
			
		||||
import eu.kanade.tachiyomi.data.cache.CoverCache
 | 
			
		||||
import eu.kanade.tachiyomi.data.coil.MangaCoverFetcher.Companion.USE_CUSTOM_COVER
 | 
			
		||||
import eu.kanade.tachiyomi.data.database.models.Manga
 | 
			
		||||
import eu.kanade.tachiyomi.network.NetworkHelper
 | 
			
		||||
import eu.kanade.tachiyomi.network.await
 | 
			
		||||
import eu.kanade.tachiyomi.source.SourceManager
 | 
			
		||||
import eu.kanade.tachiyomi.source.online.HttpSource
 | 
			
		||||
@@ -20,130 +20,181 @@ import okhttp3.CacheControl
 | 
			
		||||
import okhttp3.Call
 | 
			
		||||
import okhttp3.Request
 | 
			
		||||
import okhttp3.Response
 | 
			
		||||
import okhttp3.ResponseBody
 | 
			
		||||
import okio.buffer
 | 
			
		||||
import okio.sink
 | 
			
		||||
import okio.source
 | 
			
		||||
import uy.kohesive.injekt.Injekt
 | 
			
		||||
import uy.kohesive.injekt.api.get
 | 
			
		||||
import okhttp3.internal.closeQuietly
 | 
			
		||||
import okio.Path.Companion.toOkioPath
 | 
			
		||||
import uy.kohesive.injekt.injectLazy
 | 
			
		||||
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:
 | 
			
		||||
 * - [USE_CUSTOM_COVER]: Use custom cover if set by user, default is true
 | 
			
		||||
 */
 | 
			
		||||
class MangaCoverFetcher : Fetcher<Manga> {
 | 
			
		||||
    private val coverCache: CoverCache by injectLazy()
 | 
			
		||||
    private val sourceManager: SourceManager by injectLazy()
 | 
			
		||||
    private val defaultClient = Injekt.get<NetworkHelper>().coilClient
 | 
			
		||||
class MangaCoverFetcher(
 | 
			
		||||
    private val manga: Manga,
 | 
			
		||||
    private val sourceLazy: Lazy<HttpSource?>,
 | 
			
		||||
    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? {
 | 
			
		||||
        if (data.thumbnail_url.isNullOrBlank()) return null
 | 
			
		||||
        return data.thumbnail_url!!
 | 
			
		||||
    }
 | 
			
		||||
    // For non-custom cover
 | 
			
		||||
    private val diskCacheKey: String? by lazy { MangaCoverKeyer().key(manga, options) }
 | 
			
		||||
    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
 | 
			
		||||
        val useCustomCover = options.parameters[USE_CUSTOM_COVER] as? Boolean ?: true
 | 
			
		||||
        val customCoverFile = coverCache.getCustomCoverFile(data)
 | 
			
		||||
        val useCustomCover = options.parameters.value(USE_CUSTOM_COVER) ?: true
 | 
			
		||||
        val customCoverFile = coverCache.getCustomCoverFile(manga)
 | 
			
		||||
        if (useCustomCover && customCoverFile.exists()) {
 | 
			
		||||
            return fileLoader(customCoverFile)
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        val cover = data.thumbnail_url
 | 
			
		||||
        return when (getResourceType(cover)) {
 | 
			
		||||
            Type.URL -> httpLoader(data, options)
 | 
			
		||||
            Type.File -> fileLoader(data)
 | 
			
		||||
        // diskCacheKey is thumbnail_url
 | 
			
		||||
        url = diskCacheKey ?: error("No cover specified")
 | 
			
		||||
        return when (getResourceType(url)) {
 | 
			
		||||
            Type.URL -> httpLoader()
 | 
			
		||||
            Type.File -> fileLoader(File(url.substringAfter("file://")))
 | 
			
		||||
            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
 | 
			
		||||
        val coverCacheFile = if (manga.favorite) {
 | 
			
		||||
            coverCache.getCoverFile(manga) ?: error("No cover specified")
 | 
			
		||||
        } else {
 | 
			
		||||
            null
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (coverCacheFile?.exists() == true && options.diskCachePolicy.readEnabled) {
 | 
			
		||||
            return fileLoader(coverCacheFile)
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        val (response, body) = awaitGetCall(manga, options)
 | 
			
		||||
        if (!response.isSuccessful) {
 | 
			
		||||
            body.close()
 | 
			
		||||
        var snapshot = readFromDiskCache()
 | 
			
		||||
        try {
 | 
			
		||||
            // Fetch from disk cache
 | 
			
		||||
            if (snapshot != null) {
 | 
			
		||||
                return SourceResult(
 | 
			
		||||
                    source = snapshot.toImageSource(),
 | 
			
		||||
                    mimeType = "image/*",
 | 
			
		||||
                    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
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
        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)
 | 
			
		||||
                }
 | 
			
		||||
    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 SourceResult(
 | 
			
		||||
            source = body.source(),
 | 
			
		||||
            mimeType = "image/*",
 | 
			
		||||
            dataSource = if (response.cacheResponse != null) DataSource.DISK else DataSource.NETWORK
 | 
			
		||||
        )
 | 
			
		||||
        return request.build()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    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 readFromDiskCache(): DiskCache.Snapshot? {
 | 
			
		||||
        return if (options.diskCachePolicy.readEnabled) diskCacheLazy.value[diskCacheKey!!] else null
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    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)
 | 
			
		||||
    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)
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            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)
 | 
			
		||||
                }
 | 
			
		||||
            return editor.commitAndGet()
 | 
			
		||||
        } catch (e: Exception) {
 | 
			
		||||
            try {
 | 
			
		||||
                editor.abort()
 | 
			
		||||
            } catch (ignored: Exception) {
 | 
			
		||||
            }
 | 
			
		||||
        }.build()
 | 
			
		||||
 | 
			
		||||
        val client = source?.client?.newBuilder()?.cache(defaultClient.cache)?.build() ?: defaultClient
 | 
			
		||||
        return client.newCall(request)
 | 
			
		||||
            throw e
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    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/*",
 | 
			
		||||
            dataSource = DataSource.DISK
 | 
			
		||||
        )
 | 
			
		||||
    private fun DiskCache.Snapshot.toImageSource(): ImageSource {
 | 
			
		||||
        return ImageSource(file = data, diskCacheKey = diskCacheKey, closeable = this)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun getResourceType(cover: String?): Type? {
 | 
			
		||||
@@ -159,6 +210,20 @@ class MangaCoverFetcher : Fetcher<Manga> {
 | 
			
		||||
        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 {
 | 
			
		||||
        const val USE_CUSTOM_COVER = "use_custom_cover"
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -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() }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,13 +1,14 @@
 | 
			
		||||
package eu.kanade.tachiyomi.data.coil
 | 
			
		||||
 | 
			
		||||
import android.content.res.Resources
 | 
			
		||||
import android.os.Build
 | 
			
		||||
import androidx.core.graphics.drawable.toDrawable
 | 
			
		||||
import coil.bitmap.BitmapPool
 | 
			
		||||
import coil.ImageLoader
 | 
			
		||||
import coil.decode.DecodeResult
 | 
			
		||||
import coil.decode.Decoder
 | 
			
		||||
import coil.decode.Options
 | 
			
		||||
import coil.size.Size
 | 
			
		||||
import coil.decode.ImageDecoderDecoder
 | 
			
		||||
import coil.decode.ImageSource
 | 
			
		||||
import coil.fetch.SourceResult
 | 
			
		||||
import coil.request.Options
 | 
			
		||||
import eu.kanade.tachiyomi.util.system.ImageUtil
 | 
			
		||||
import okio.BufferedSource
 | 
			
		||||
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.
 | 
			
		||||
 */
 | 
			
		||||
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 {
 | 
			
		||||
        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 suspend fun decode(
 | 
			
		||||
        pool: BitmapPool,
 | 
			
		||||
        source: BufferedSource,
 | 
			
		||||
        size: Size,
 | 
			
		||||
        options: Options
 | 
			
		||||
    ): DecodeResult {
 | 
			
		||||
        val decoder = source.use {
 | 
			
		||||
    override suspend fun decode(): DecodeResult {
 | 
			
		||||
        val decoder = resources.sourceOrNull()?.use {
 | 
			
		||||
            ImageDecoder.newInstance(it.inputStream())
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
@@ -46,8 +31,31 @@ class TachiyomiImageDecoder(private val resources: Resources) : Decoder {
 | 
			
		||||
        check(bitmap != null) { "Failed to decode image." }
 | 
			
		||||
 | 
			
		||||
        return DecodeResult(
 | 
			
		||||
            drawable = bitmap.toDrawable(resources),
 | 
			
		||||
            drawable = bitmap.toDrawable(options.context.resources),
 | 
			
		||||
            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()
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,6 @@
 | 
			
		||||
package eu.kanade.tachiyomi.network
 | 
			
		||||
 | 
			
		||||
import android.content.Context
 | 
			
		||||
import coil.util.CoilUtils
 | 
			
		||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
 | 
			
		||||
import eu.kanade.tachiyomi.network.interceptor.CloudflareInterceptor
 | 
			
		||||
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 coilClient by lazy { baseClientBuilder.cache(CoilUtils.createDefaultCache(context)).build() }
 | 
			
		||||
 | 
			
		||||
    val cloudflareClient by lazy {
 | 
			
		||||
        client.newBuilder()
 | 
			
		||||
            .addInterceptor(CloudflareInterceptor(context))
 | 
			
		||||
 
 | 
			
		||||
@@ -2,7 +2,7 @@ package eu.kanade.tachiyomi.ui.browse.extension
 | 
			
		||||
 | 
			
		||||
import android.view.View
 | 
			
		||||
import androidx.core.view.isVisible
 | 
			
		||||
import coil.clear
 | 
			
		||||
import coil.dispose
 | 
			
		||||
import coil.load
 | 
			
		||||
import eu.davidea.viewholders.FlexibleViewHolder
 | 
			
		||||
import eu.kanade.tachiyomi.R
 | 
			
		||||
@@ -39,7 +39,7 @@ class ExtensionHolder(view: View, val adapter: ExtensionAdapter) :
 | 
			
		||||
            else -> ""
 | 
			
		||||
        }.uppercase()
 | 
			
		||||
 | 
			
		||||
        binding.icon.clear()
 | 
			
		||||
        binding.icon.dispose()
 | 
			
		||||
        if (extension is Extension.Available) {
 | 
			
		||||
            binding.icon.load(extension.iconUrl)
 | 
			
		||||
        } else if (extension is Extension.Installed) {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,8 +1,8 @@
 | 
			
		||||
package eu.kanade.tachiyomi.ui.browse.migration.manga
 | 
			
		||||
 | 
			
		||||
import android.view.View
 | 
			
		||||
import coil.clear
 | 
			
		||||
import coil.loadAny
 | 
			
		||||
import coil.dispose
 | 
			
		||||
import coil.load
 | 
			
		||||
import eu.davidea.viewholders.FlexibleViewHolder
 | 
			
		||||
import eu.kanade.tachiyomi.databinding.SourceListItemBinding
 | 
			
		||||
 | 
			
		||||
@@ -23,7 +23,7 @@ class MigrationMangaHolder(
 | 
			
		||||
        binding.title.text = item.manga.title
 | 
			
		||||
 | 
			
		||||
        // Update the cover
 | 
			
		||||
        binding.thumbnail.clear()
 | 
			
		||||
        binding.thumbnail.loadAny(item.manga)
 | 
			
		||||
        binding.thumbnail.dispose()
 | 
			
		||||
        binding.thumbnail.load(item.manga)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,7 @@
 | 
			
		||||
package eu.kanade.tachiyomi.ui.browse.source.browse
 | 
			
		||||
 | 
			
		||||
import androidx.core.view.isVisible
 | 
			
		||||
import coil.clear
 | 
			
		||||
import coil.dispose
 | 
			
		||||
import coil.imageLoader
 | 
			
		||||
import coil.request.ImageRequest
 | 
			
		||||
import coil.transition.CrossfadeTransition
 | 
			
		||||
@@ -48,10 +48,10 @@ class SourceComfortableGridHolder(
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun setImage(manga: Manga) {
 | 
			
		||||
        binding.thumbnail.clear()
 | 
			
		||||
        binding.thumbnail.dispose()
 | 
			
		||||
        if (!manga.thumbnail_url.isNullOrEmpty()) {
 | 
			
		||||
            val crossfadeDuration = binding.root.context.imageLoader.defaults.transition.let {
 | 
			
		||||
                if (it is CrossfadeTransition) it.durationMillis else 0
 | 
			
		||||
            val crossfadeDuration = binding.root.context.imageLoader.defaults.transitionFactory.let {
 | 
			
		||||
                if (it is CrossfadeTransition.Factory) it.durationMillis else 0
 | 
			
		||||
            }
 | 
			
		||||
            val request = ImageRequest.Builder(binding.root.context)
 | 
			
		||||
                .data(manga)
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,7 @@
 | 
			
		||||
package eu.kanade.tachiyomi.ui.browse.source.browse
 | 
			
		||||
 | 
			
		||||
import androidx.core.view.isVisible
 | 
			
		||||
import coil.clear
 | 
			
		||||
import coil.dispose
 | 
			
		||||
import coil.imageLoader
 | 
			
		||||
import coil.request.ImageRequest
 | 
			
		||||
import coil.transition.CrossfadeTransition
 | 
			
		||||
@@ -48,10 +48,10 @@ class SourceCompactGridHolder(
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun setImage(manga: Manga) {
 | 
			
		||||
        binding.thumbnail.clear()
 | 
			
		||||
        binding.thumbnail.dispose()
 | 
			
		||||
        if (!manga.thumbnail_url.isNullOrEmpty()) {
 | 
			
		||||
            val crossfadeDuration = binding.root.context.imageLoader.defaults.transition.let {
 | 
			
		||||
                if (it is CrossfadeTransition) it.durationMillis else 0
 | 
			
		||||
            val crossfadeDuration = binding.root.context.imageLoader.defaults.transitionFactory.let {
 | 
			
		||||
                if (it is CrossfadeTransition.Factory) it.durationMillis else 0
 | 
			
		||||
            }
 | 
			
		||||
            val request = ImageRequest.Builder(binding.root.context)
 | 
			
		||||
                .data(manga)
 | 
			
		||||
 
 | 
			
		||||
@@ -2,8 +2,8 @@ package eu.kanade.tachiyomi.ui.browse.source.browse
 | 
			
		||||
 | 
			
		||||
import android.view.View
 | 
			
		||||
import androidx.core.view.isVisible
 | 
			
		||||
import coil.clear
 | 
			
		||||
import coil.loadAny
 | 
			
		||||
import coil.dispose
 | 
			
		||||
import coil.load
 | 
			
		||||
import eu.davidea.flexibleadapter.FlexibleAdapter
 | 
			
		||||
import eu.kanade.tachiyomi.R
 | 
			
		||||
import eu.kanade.tachiyomi.data.coil.MangaCoverFetcher
 | 
			
		||||
@@ -50,9 +50,9 @@ class SourceListHolder(private val view: View, adapter: FlexibleAdapter<*>) :
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun setImage(manga: Manga) {
 | 
			
		||||
        binding.thumbnail.clear()
 | 
			
		||||
        binding.thumbnail.dispose()
 | 
			
		||||
        if (!manga.thumbnail_url.isNullOrEmpty()) {
 | 
			
		||||
            binding.thumbnail.loadAny(manga) {
 | 
			
		||||
            binding.thumbnail.load(manga) {
 | 
			
		||||
                setParameter(MangaCoverFetcher.USE_CUSTOM_COVER, false)
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 
 | 
			
		||||
@@ -2,7 +2,7 @@ package eu.kanade.tachiyomi.ui.browse.source.globalsearch
 | 
			
		||||
 | 
			
		||||
import android.view.View
 | 
			
		||||
import androidx.core.view.isVisible
 | 
			
		||||
import coil.clear
 | 
			
		||||
import coil.dispose
 | 
			
		||||
import coil.imageLoader
 | 
			
		||||
import coil.request.ImageRequest
 | 
			
		||||
import coil.transition.CrossfadeTransition
 | 
			
		||||
@@ -53,10 +53,10 @@ class GlobalSearchCardHolder(view: View, adapter: GlobalSearchCardAdapter) :
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun setImage(manga: Manga) {
 | 
			
		||||
        binding.cover.clear()
 | 
			
		||||
        binding.cover.dispose()
 | 
			
		||||
        if (!manga.thumbnail_url.isNullOrEmpty()) {
 | 
			
		||||
            val crossfadeDuration = itemView.context.imageLoader.defaults.transition.let {
 | 
			
		||||
                if (it is CrossfadeTransition) it.durationMillis else 0
 | 
			
		||||
            val crossfadeDuration = itemView.context.imageLoader.defaults.transitionFactory.let {
 | 
			
		||||
                if (it is CrossfadeTransition.Factory) it.durationMillis else 0
 | 
			
		||||
            }
 | 
			
		||||
            val request = ImageRequest.Builder(itemView.context)
 | 
			
		||||
                .data(manga)
 | 
			
		||||
 
 | 
			
		||||
@@ -2,11 +2,11 @@ package eu.kanade.tachiyomi.ui.library
 | 
			
		||||
 | 
			
		||||
import androidx.core.view.isVisible
 | 
			
		||||
import androidx.recyclerview.widget.RecyclerView
 | 
			
		||||
import coil.clear
 | 
			
		||||
import coil.dispose
 | 
			
		||||
import eu.davidea.flexibleadapter.FlexibleAdapter
 | 
			
		||||
import eu.davidea.flexibleadapter.items.IFlexible
 | 
			
		||||
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.
 | 
			
		||||
@@ -55,7 +55,7 @@ class LibraryComfortableGridHolder(
 | 
			
		||||
        binding.badges.localText.isVisible = item.isLocal
 | 
			
		||||
 | 
			
		||||
        // Update the cover.
 | 
			
		||||
        binding.thumbnail.clear()
 | 
			
		||||
        binding.thumbnail.loadAnyAutoPause(item.manga)
 | 
			
		||||
        binding.thumbnail.dispose()
 | 
			
		||||
        binding.thumbnail.loadAutoPause(item.manga)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,10 +1,10 @@
 | 
			
		||||
package eu.kanade.tachiyomi.ui.library
 | 
			
		||||
 | 
			
		||||
import androidx.core.view.isVisible
 | 
			
		||||
import coil.clear
 | 
			
		||||
import coil.dispose
 | 
			
		||||
import eu.davidea.flexibleadapter.FlexibleAdapter
 | 
			
		||||
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.
 | 
			
		||||
@@ -54,11 +54,11 @@ class LibraryCompactGridHolder(
 | 
			
		||||
        binding.badges.localText.isVisible = item.isLocal
 | 
			
		||||
 | 
			
		||||
        // Update the cover.
 | 
			
		||||
        binding.thumbnail.clear()
 | 
			
		||||
        binding.thumbnail.dispose()
 | 
			
		||||
        if (coverOnly) {
 | 
			
		||||
            // Cover only mode: Hides title text unless thumbnail is unavailable
 | 
			
		||||
            if (!item.manga.thumbnail_url.isNullOrEmpty()) {
 | 
			
		||||
                binding.thumbnail.loadAnyAutoPause(item.manga)
 | 
			
		||||
                binding.thumbnail.loadAutoPause(item.manga)
 | 
			
		||||
                binding.title.isVisible = false
 | 
			
		||||
            } else {
 | 
			
		||||
                binding.title.text = item.manga.title
 | 
			
		||||
@@ -66,7 +66,7 @@ class LibraryCompactGridHolder(
 | 
			
		||||
            }
 | 
			
		||||
            binding.thumbnail.foreground = null
 | 
			
		||||
        } else {
 | 
			
		||||
            binding.thumbnail.loadAnyAutoPause(item.manga)
 | 
			
		||||
            binding.thumbnail.loadAutoPause(item.manga)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -2,8 +2,8 @@ package eu.kanade.tachiyomi.ui.library
 | 
			
		||||
 | 
			
		||||
import android.view.View
 | 
			
		||||
import androidx.core.view.isVisible
 | 
			
		||||
import coil.clear
 | 
			
		||||
import coil.loadAny
 | 
			
		||||
import coil.dispose
 | 
			
		||||
import coil.load
 | 
			
		||||
import eu.davidea.flexibleadapter.FlexibleAdapter
 | 
			
		||||
import eu.kanade.tachiyomi.databinding.SourceListItemBinding
 | 
			
		||||
 | 
			
		||||
@@ -61,7 +61,7 @@ class LibraryListHolder(
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Update the cover
 | 
			
		||||
        binding.thumbnail.clear()
 | 
			
		||||
        binding.thumbnail.loadAny(item.manga)
 | 
			
		||||
        binding.thumbnail.dispose()
 | 
			
		||||
        binding.thumbnail.load(item.manga)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -334,7 +334,7 @@ class MangaPresenter(
 | 
			
		||||
     * @return cover as Bitmap or null if there is no thumbnail cached with the memoryCacheKey
 | 
			
		||||
     */
 | 
			
		||||
    private fun coverBitmapFromImageLoader(context: Context, memoryCacheKey: MemoryCache.Key): Bitmap? {
 | 
			
		||||
        return context.imageLoader.memoryCache[memoryCacheKey]
 | 
			
		||||
        return context.imageLoader.memoryCache?.get(memoryCacheKey)?.bitmap
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
 
 | 
			
		||||
@@ -19,7 +19,7 @@ import eu.kanade.tachiyomi.source.online.HttpSource
 | 
			
		||||
import eu.kanade.tachiyomi.ui.base.controller.getMainAppBarHeight
 | 
			
		||||
import eu.kanade.tachiyomi.ui.manga.MangaController
 | 
			
		||||
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.onEach
 | 
			
		||||
import reactivecircus.flowbinding.android.view.clicks
 | 
			
		||||
@@ -286,8 +286,8 @@ class MangaInfoHeaderAdapter(
 | 
			
		||||
            setFavoriteButtonState(manga.favorite)
 | 
			
		||||
 | 
			
		||||
            // Set cover if changed.
 | 
			
		||||
            binding.backdrop.loadAnyAutoPause(manga)
 | 
			
		||||
            binding.mangaCover.loadAnyAutoPause(manga)
 | 
			
		||||
            binding.backdrop.loadAutoPause(manga)
 | 
			
		||||
            binding.mangaCover.loadAutoPause(manga)
 | 
			
		||||
 | 
			
		||||
            // Manga info section
 | 
			
		||||
            binding.mangaSummarySection.setTags(manga.getGenres(), controller::performGenreSearch)
 | 
			
		||||
 
 | 
			
		||||
@@ -2,8 +2,8 @@ package eu.kanade.tachiyomi.ui.manga.track
 | 
			
		||||
 | 
			
		||||
import androidx.core.view.isVisible
 | 
			
		||||
import androidx.recyclerview.widget.RecyclerView
 | 
			
		||||
import coil.clear
 | 
			
		||||
import coil.loadAny
 | 
			
		||||
import coil.dispose
 | 
			
		||||
import coil.load
 | 
			
		||||
import eu.kanade.tachiyomi.data.track.model.TrackSearch
 | 
			
		||||
import eu.kanade.tachiyomi.databinding.TrackSearchItemBinding
 | 
			
		||||
import java.util.Locale
 | 
			
		||||
@@ -20,9 +20,9 @@ class TrackSearchHolder(
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        binding.trackSearchTitle.text = track.title
 | 
			
		||||
        binding.trackSearchCover.clear()
 | 
			
		||||
        binding.trackSearchCover.dispose()
 | 
			
		||||
        if (track.cover_url.isNotEmpty()) {
 | 
			
		||||
            binding.trackSearchCover.loadAny(track.cover_url)
 | 
			
		||||
            binding.trackSearchCover.load(track.cover_url)
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        val hasStatus = track.publishing_status.isNotBlank()
 | 
			
		||||
 
 | 
			
		||||
@@ -17,7 +17,7 @@ import androidx.annotation.CallSuper
 | 
			
		||||
import androidx.annotation.StyleRes
 | 
			
		||||
import androidx.appcompat.widget.AppCompatImageView
 | 
			
		||||
import androidx.core.view.isVisible
 | 
			
		||||
import coil.clear
 | 
			
		||||
import coil.dispose
 | 
			
		||||
import coil.imageLoader
 | 
			
		||||
import coil.request.CachePolicy
 | 
			
		||||
import coil.request.ImageRequest
 | 
			
		||||
@@ -152,7 +152,7 @@ open class ReaderPageImageView @JvmOverloads constructor(
 | 
			
		||||
    fun recycle() = pageView?.let {
 | 
			
		||||
        when (it) {
 | 
			
		||||
            is SubsamplingScaleImageView -> it.recycle()
 | 
			
		||||
            is AppCompatImageView -> it.clear()
 | 
			
		||||
            is AppCompatImageView -> it.dispose()
 | 
			
		||||
        }
 | 
			
		||||
        it.isVisible = false
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -1,8 +1,8 @@
 | 
			
		||||
package eu.kanade.tachiyomi.ui.recent.history
 | 
			
		||||
 | 
			
		||||
import android.view.View
 | 
			
		||||
import coil.clear
 | 
			
		||||
import coil.loadAny
 | 
			
		||||
import coil.dispose
 | 
			
		||||
import coil.load
 | 
			
		||||
import eu.davidea.viewholders.FlexibleViewHolder
 | 
			
		||||
import eu.kanade.tachiyomi.R
 | 
			
		||||
import eu.kanade.tachiyomi.data.database.models.MangaChapterHistory
 | 
			
		||||
@@ -65,7 +65,7 @@ class HistoryHolder(
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Set cover
 | 
			
		||||
        binding.cover.clear()
 | 
			
		||||
        binding.cover.loadAny(item.manga)
 | 
			
		||||
        binding.cover.dispose()
 | 
			
		||||
        binding.cover.load(item.manga)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -2,8 +2,8 @@ package eu.kanade.tachiyomi.ui.recent.updates
 | 
			
		||||
 | 
			
		||||
import android.view.View
 | 
			
		||||
import androidx.core.view.isVisible
 | 
			
		||||
import coil.clear
 | 
			
		||||
import coil.loadAny
 | 
			
		||||
import coil.dispose
 | 
			
		||||
import coil.load
 | 
			
		||||
import eu.kanade.tachiyomi.databinding.UpdatesItemBinding
 | 
			
		||||
import eu.kanade.tachiyomi.source.LocalSource
 | 
			
		||||
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)
 | 
			
		||||
 | 
			
		||||
        // Set cover
 | 
			
		||||
        binding.mangaCover.clear()
 | 
			
		||||
        binding.mangaCover.loadAny(item.manga)
 | 
			
		||||
        binding.mangaCover.dispose()
 | 
			
		||||
        binding.mangaCover.load(item.manga)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -8,7 +8,7 @@ import androidx.annotation.DrawableRes
 | 
			
		||||
import androidx.appcompat.content.res.AppCompatResources
 | 
			
		||||
import coil.ImageLoader
 | 
			
		||||
import coil.imageLoader
 | 
			
		||||
import coil.loadAny
 | 
			
		||||
import coil.load
 | 
			
		||||
import coil.request.ImageRequest
 | 
			
		||||
import coil.target.ImageViewTarget
 | 
			
		||||
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
 | 
			
		||||
 * if [Context.animatorDurationScale] is 0
 | 
			
		||||
 */
 | 
			
		||||
fun ImageView.loadAnyAutoPause(
 | 
			
		||||
fun ImageView.loadAutoPause(
 | 
			
		||||
    data: Any?,
 | 
			
		||||
    loader: ImageLoader = context.imageLoader,
 | 
			
		||||
    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
 | 
			
		||||
        val originalBuild = apply(builder).build()
 | 
			
		||||
        listener(
 | 
			
		||||
 
 | 
			
		||||
@@ -2,7 +2,7 @@
 | 
			
		||||
aboutlib_version = "8.9.4"
 | 
			
		||||
okhttp_version = "4.9.1"
 | 
			
		||||
nucleus_version = "3.0.0"
 | 
			
		||||
coil_version = "1.4.0"
 | 
			
		||||
coil_version = "2.0.0-rc01"
 | 
			
		||||
conductor_version = "3.1.2"
 | 
			
		||||
flowbinding_version = "1.2.0"
 | 
			
		||||
shizuku_version = "12.1.0"
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user