mirror of
				https://github.com/mihonapp/mihon.git
				synced 2025-10-31 14:27:57 +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( | ||||
|   | ||||
		Reference in New Issue
	
	Block a user