From 10eef282fa2fc0d8ae919d8b86bff1b529b4330d Mon Sep 17 00:00:00 2001 From: Ivan Iskandar <12537387+ivaniskandar@users.noreply.github.com> Date: Sat, 5 Mar 2022 04:04:32 +0700 Subject: [PATCH] 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 --- app/src/main/java/eu/kanade/tachiyomi/App.kt | 41 ++- .../kanade/tachiyomi/data/cache/CoverCache.kt | 2 +- .../tachiyomi/data/coil/ByteBufferFetcher.kt | 25 -- .../tachiyomi/data/coil/MangaCoverFetcher.kt | 237 +++++++++++------- .../tachiyomi/data/coil/MangaCoverKeyer.kt | 11 + .../data/coil/TachiyomiImageDecoder.kt | 56 +++-- .../kanade/tachiyomi/network/NetworkHelper.kt | 3 - .../ui/browse/extension/ExtensionHolder.kt | 4 +- .../migration/manga/MigrationMangaHolder.kt | 8 +- .../browse/SourceComfortableGridHolder.kt | 8 +- .../source/browse/SourceCompactGridHolder.kt | 8 +- .../browse/source/browse/SourceListHolder.kt | 8 +- .../globalsearch/GlobalSearchCardHolder.kt | 8 +- .../library/LibraryComfortableGridHolder.kt | 8 +- .../ui/library/LibraryCompactGridHolder.kt | 10 +- .../tachiyomi/ui/library/LibraryListHolder.kt | 8 +- .../tachiyomi/ui/manga/MangaPresenter.kt | 2 +- .../ui/manga/info/MangaInfoHeaderAdapter.kt | 6 +- .../ui/manga/track/TrackSearchHolder.kt | 8 +- .../ui/reader/viewer/ReaderPageImageView.kt | 4 +- .../ui/recent/history/HistoryHolder.kt | 8 +- .../ui/recent/updates/UpdatesHolder.kt | 8 +- .../util/view/ImageViewExtensions.kt | 7 +- gradle/libs.versions.toml | 2 +- 24 files changed, 286 insertions(+), 204 deletions(-) delete mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/coil/ByteBufferFetcher.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/coil/MangaCoverKeyer.kt diff --git a/app/src/main/java/eu/kanade/tachiyomi/App.kt b/app/src/main/java/eu/kanade/tachiyomi/App.kt index c29fe5cfc2..53b9417d23 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/App.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/App.kt @@ -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().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().coilClient) + callFactory(callFactoryInit) + diskCache(diskCacheInit) crossfade((300 * this@App.animatorDurationScale).toInt()) allowRgb565(getSystemService()!!.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 } + } + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/cache/CoverCache.kt b/app/src/main/java/eu/kanade/tachiyomi/data/cache/CoverCache.kt index fbdfc52bd0..123d697a7b 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/cache/CoverCache.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/cache/CoverCache.kt @@ -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 { diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/coil/ByteBufferFetcher.kt b/app/src/main/java/eu/kanade/tachiyomi/data/coil/ByteBufferFetcher.kt deleted file mode 100644 index 78d9c7ecd3..0000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/data/coil/ByteBufferFetcher.kt +++ /dev/null @@ -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 { - 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 -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/coil/MangaCoverFetcher.kt b/app/src/main/java/eu/kanade/tachiyomi/data/coil/MangaCoverFetcher.kt index 7dab6c7dab..49be61061c 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/coil/MangaCoverFetcher.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/coil/MangaCoverFetcher.kt @@ -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 { - private val coverCache: CoverCache by injectLazy() - private val sourceManager: SourceManager by injectLazy() - private val defaultClient = Injekt.get().coilClient +class MangaCoverFetcher( + private val manga: Manga, + private val sourceLazy: Lazy, + private val options: Options, + private val coverCache: CoverCache, + private val callFactoryLazy: Lazy, + private val diskCacheLazy: Lazy +) : 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 { - 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 { File, URL } + class Factory( + private val callFactoryLazy: Lazy, + private val diskCacheLazy: Lazy + ) : Fetcher.Factory { + + 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" diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/coil/MangaCoverKeyer.kt b/app/src/main/java/eu/kanade/tachiyomi/data/coil/MangaCoverKeyer.kt new file mode 100644 index 0000000000..6f3ce70ae7 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/coil/MangaCoverKeyer.kt @@ -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 { + override fun key(data: Manga, options: Options): String? { + return data.thumbnail_url?.takeIf { it.isNotBlank() } + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/coil/TachiyomiImageDecoder.kt b/app/src/main/java/eu/kanade/tachiyomi/data/coil/TachiyomiImageDecoder.kt index 7847e6f4f4..b8545b9634 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/coil/TachiyomiImageDecoder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/coil/TachiyomiImageDecoder.kt @@ -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() + } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/network/NetworkHelper.kt b/app/src/main/java/eu/kanade/tachiyomi/network/NetworkHelper.kt index fed5d23be0..6fadc41520 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/network/NetworkHelper.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/network/NetworkHelper.kt @@ -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)) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/ExtensionHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/ExtensionHolder.kt index 718e146d33..019ae65ff0 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/ExtensionHolder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/ExtensionHolder.kt @@ -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) { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/manga/MigrationMangaHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/manga/MigrationMangaHolder.kt index b74c4fece0..7e2c73c7b6 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/manga/MigrationMangaHolder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/manga/MigrationMangaHolder.kt @@ -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) } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/SourceComfortableGridHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/SourceComfortableGridHolder.kt index ebcbc16c83..4983a0f125 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/SourceComfortableGridHolder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/SourceComfortableGridHolder.kt @@ -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) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/SourceCompactGridHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/SourceCompactGridHolder.kt index 0bc97039a3..a502c4c31f 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/SourceCompactGridHolder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/SourceCompactGridHolder.kt @@ -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) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/SourceListHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/SourceListHolder.kt index e12b963510..91855ef9d0 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/SourceListHolder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/SourceListHolder.kt @@ -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) } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/GlobalSearchCardHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/GlobalSearchCardHolder.kt index a67e9785bc..fd31f10057 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/GlobalSearchCardHolder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/GlobalSearchCardHolder.kt @@ -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) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryComfortableGridHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryComfortableGridHolder.kt index 74d6201d36..3f63340170 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryComfortableGridHolder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryComfortableGridHolder.kt @@ -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) } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryCompactGridHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryCompactGridHolder.kt index c92c4a64a3..4e753d5273 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryCompactGridHolder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryCompactGridHolder.kt @@ -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) } } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryListHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryListHolder.kt index c6500ef6ac..a1dd481b40 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryListHolder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryListHolder.kt @@ -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) } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaPresenter.kt index c2a2579a08..243c606c22 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaPresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaPresenter.kt @@ -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 } /** diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaInfoHeaderAdapter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaInfoHeaderAdapter.kt index 816a91d14c..b261001163 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaInfoHeaderAdapter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaInfoHeaderAdapter.kt @@ -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) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackSearchHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackSearchHolder.kt index 4183a93ea3..beabf2ebe3 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackSearchHolder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackSearchHolder.kt @@ -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() diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/ReaderPageImageView.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/ReaderPageImageView.kt index f60e911583..6a1a2bbe61 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/ReaderPageImageView.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/ReaderPageImageView.kt @@ -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 } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/recent/history/HistoryHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/recent/history/HistoryHolder.kt index a3ac1dea35..6ef3a38cd0 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/recent/history/HistoryHolder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/recent/history/HistoryHolder.kt @@ -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) } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/recent/updates/UpdatesHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/recent/updates/UpdatesHolder.kt index 76bb24bda6..8fcdd22e0f 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/recent/updates/UpdatesHolder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/recent/updates/UpdatesHolder.kt @@ -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) } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/view/ImageViewExtensions.kt b/app/src/main/java/eu/kanade/tachiyomi/util/view/ImageViewExtensions.kt index 3bca8603ce..a8fddfd390 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/util/view/ImageViewExtensions.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/util/view/ImageViewExtensions.kt @@ -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( diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 230e41f076..e936bcbe91 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -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"