Coil 2.x upgrade (#6725)

* Migrate to Coil 2

* Adapt to use coil disk cache

* Update to alpha 7

* Update to alpha 8

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

View File

@ -20,9 +20,10 @@ import coil.ImageLoader
import coil.ImageLoaderFactory
import coil.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 }
}
}
}

View File

@ -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 {

View File

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

View File

@ -1,18 +1,18 @@
package eu.kanade.tachiyomi.data.coil
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,132 +20,183 @@ 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()
throw HttpException(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)
}
}
}
var snapshot = readFromDiskCache()
try {
// Fetch from disk cache
if (snapshot != null) {
return SourceResult(
source = body.source(),
mimeType = "image/*",
dataSource = if (response.cacheResponse != null) DataSource.DISK else DataSource.NETWORK
)
}
private suspend fun awaitGetCall(manga: Manga, options: Options): Pair<Response, ResponseBody> {
val call = getCall(manga, options)
val response = call.await()
return response to checkNotNull(response.body) { "Null response source" }
}
private fun getCall(manga: Manga, options: Options): Call {
val source = sourceManager.get(manga.source) as? HttpSource
val request = Request.Builder().url(manga.thumbnail_url!!).also {
if (source != null) {
it.headers(source.headers)
}
val networkRead = options.networkCachePolicy.readEnabled
val diskRead = options.diskCachePolicy.readEnabled
when {
!networkRead && diskRead -> {
it.cacheControl(CacheControl.FORCE_CACHE)
}
networkRead && !diskRead -> if (options.diskCachePolicy.writeEnabled) {
it.cacheControl(CacheControl.FORCE_NETWORK)
} else {
it.cacheControl(CACHE_CONTROL_FORCE_NETWORK_NO_CACHE)
}
!networkRead && !diskRead -> {
// This causes the request to fail with a 504 Unsatisfiable Request.
it.cacheControl(CACHE_CONTROL_NO_NETWORK_NO_CACHE)
}
}
}.build()
val client = source?.client?.newBuilder()?.cache(defaultClient.cache)?.build() ?: defaultClient
return client.newCall(request)
}
private fun fileLoader(manga: Manga): FetchResult {
return fileLoader(File(manga.thumbnail_url!!.substringAfter("file://")))
}
private fun fileLoader(file: File): FetchResult {
return SourceResult(
source = file.source().buffer(),
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
}
private fun newRequest(): Request {
val request = Request.Builder()
.url(url)
.headers(options.headers)
// Support attaching custom data to the network request.
.tag(Parameters::class.java, options.parameters)
val diskRead = options.diskCachePolicy.readEnabled
val networkRead = options.networkCachePolicy.readEnabled
when {
!networkRead && diskRead -> {
request.cacheControl(CacheControl.FORCE_CACHE)
}
networkRead && !diskRead -> if (options.diskCachePolicy.writeEnabled) {
request.cacheControl(CacheControl.FORCE_NETWORK)
} else {
request.cacheControl(CACHE_CONTROL_FORCE_NETWORK_NO_CACHE)
}
!networkRead && !diskRead -> {
// This causes the request to fail with a 504 Unsatisfiable Request.
request.cacheControl(CACHE_CONTROL_NO_NETWORK_NO_CACHE)
}
}
return request.build()
}
private fun readFromDiskCache(): DiskCache.Snapshot? {
return if (options.diskCachePolicy.readEnabled) diskCacheLazy.value[diskCacheKey!!] else null
}
private fun writeToDiskCache(snapshot: DiskCache.Snapshot?, response: Response): DiskCache.Snapshot? {
if (!options.diskCachePolicy.writeEnabled) {
snapshot?.closeQuietly()
return null
}
val editor = if (snapshot != null) {
snapshot.closeAndEdit()
} else {
diskCacheLazy.value.edit(diskCacheKey!!)
} ?: return null
try {
diskCacheLazy.value.fileSystem.write(editor.data) {
response.body!!.source().readAll(this)
}
return editor.commitAndGet()
} catch (e: Exception) {
try {
editor.abort()
} catch (ignored: Exception) {
}
throw e
}
}
private fun DiskCache.Snapshot.toImageSource(): ImageSource {
return ImageSource(file = data, diskCacheKey = diskCacheKey, closeable = this)
}
private fun getResourceType(cover: String?): Type? {
return when {
cover.isNullOrEmpty() -> null
@ -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"

View File

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

View File

@ -1,13 +1,14 @@
package eu.kanade.tachiyomi.data.coil
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()
}
}

View File

@ -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))

View File

@ -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) {

View File

@ -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)
}
}

View File

@ -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)

View File

@ -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)

View File

@ -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)
}
}

View File

@ -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)

View File

@ -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)
}
}

View File

@ -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)
}
}
}

View File

@ -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)
}
}

View File

@ -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
}
/**

View File

@ -19,7 +19,7 @@ import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.ui.base.controller.getMainAppBarHeight
import eu.kanade.tachiyomi.ui.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)

View File

@ -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()

View File

@ -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
}

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -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(

View File

@ -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"