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:
parent
f312936629
commit
10eef282fa
24 changed files with 286 additions and 204 deletions
|
@ -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,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"
|
||||
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
package eu.kanade.tachiyomi.data.coil
|
||||
|
||||
import coil.key.Keyer
|
||||
import coil.request.Options
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
|
||||
class MangaCoverKeyer : Keyer<Manga> {
|
||||
override fun key(data: Manga, options: Options): String? {
|
||||
return data.thumbnail_url?.takeIf { it.isNotBlank() }
|
||||
}
|
||||
}
|
|
@ -1,13 +1,14 @@
|
|||
package eu.kanade.tachiyomi.data.coil
|
||||
|
||||
import android.content.res.Resources
|
||||
import android.os.Build
|
||||
import androidx.core.graphics.drawable.toDrawable
|
||||
import coil.bitmap.BitmapPool
|
||||
import coil.ImageLoader
|
||||
import coil.decode.DecodeResult
|
||||
import coil.decode.Decoder
|
||||
import coil.decode.Options
|
||||
import coil.size.Size
|
||||
import coil.decode.ImageDecoderDecoder
|
||||
import coil.decode.ImageSource
|
||||
import coil.fetch.SourceResult
|
||||
import coil.request.Options
|
||||
import eu.kanade.tachiyomi.util.system.ImageUtil
|
||||
import okio.BufferedSource
|
||||
import tachiyomi.decoder.ImageDecoder
|
||||
|
@ -15,26 +16,10 @@ import tachiyomi.decoder.ImageDecoder
|
|||
/**
|
||||
* A [Decoder] that uses built-in [ImageDecoder] to decode images that is not supported by the system.
|
||||
*/
|
||||
class TachiyomiImageDecoder(private val resources: Resources) : Decoder {
|
||||
class TachiyomiImageDecoder(private val resources: ImageSource, private val options: Options) : Decoder {
|
||||
|
||||
override fun handles(source: BufferedSource, mimeType: String?): Boolean {
|
||||
val type = source.peek().inputStream().use {
|
||||
ImageUtil.findImageType(it)
|
||||
}
|
||||
return when (type) {
|
||||
ImageUtil.ImageType.AVIF, ImageUtil.ImageType.JXL -> true
|
||||
ImageUtil.ImageType.HEIF -> Build.VERSION.SDK_INT < Build.VERSION_CODES.O
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun decode(
|
||||
pool: BitmapPool,
|
||||
source: BufferedSource,
|
||||
size: Size,
|
||||
options: Options
|
||||
): DecodeResult {
|
||||
val decoder = source.use {
|
||||
override suspend fun decode(): DecodeResult {
|
||||
val decoder = resources.sourceOrNull()?.use {
|
||||
ImageDecoder.newInstance(it.inputStream())
|
||||
}
|
||||
|
||||
|
@ -46,8 +31,31 @@ class TachiyomiImageDecoder(private val resources: Resources) : Decoder {
|
|||
check(bitmap != null) { "Failed to decode image." }
|
||||
|
||||
return DecodeResult(
|
||||
drawable = bitmap.toDrawable(resources),
|
||||
drawable = bitmap.toDrawable(options.context.resources),
|
||||
isSampled = false
|
||||
)
|
||||
}
|
||||
|
||||
class Factory : Decoder.Factory {
|
||||
|
||||
override fun create(result: SourceResult, options: Options, imageLoader: ImageLoader): Decoder? {
|
||||
if (!isApplicable(result.source.source())) return null
|
||||
return TachiyomiImageDecoder(result.source, options)
|
||||
}
|
||||
|
||||
private fun isApplicable(source: BufferedSource): Boolean {
|
||||
val type = source.peek().inputStream().use {
|
||||
ImageUtil.findImageType(it)
|
||||
}
|
||||
return when (type) {
|
||||
ImageUtil.ImageType.AVIF, ImageUtil.ImageType.JXL -> true
|
||||
ImageUtil.ImageType.HEIF -> Build.VERSION.SDK_INT < Build.VERSION_CODES.O
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
override fun equals(other: Any?) = other is ImageDecoderDecoder.Factory
|
||||
|
||||
override fun hashCode() = javaClass.hashCode()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
package eu.kanade.tachiyomi.network
|
||||
|
||||
import android.content.Context
|
||||
import coil.util.CoilUtils
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.network.interceptor.CloudflareInterceptor
|
||||
import eu.kanade.tachiyomi.network.interceptor.UserAgentInterceptor
|
||||
|
@ -49,8 +48,6 @@ class NetworkHelper(context: Context) {
|
|||
|
||||
val client by lazy { baseClientBuilder.cache(Cache(cacheDir, cacheSize)).build() }
|
||||
|
||||
val coilClient by lazy { baseClientBuilder.cache(CoilUtils.createDefaultCache(context)).build() }
|
||||
|
||||
val cloudflareClient by lazy {
|
||||
client.newBuilder()
|
||||
.addInterceptor(CloudflareInterceptor(context))
|
||||
|
|
|
@ -2,7 +2,7 @@ package eu.kanade.tachiyomi.ui.browse.extension
|
|||
|
||||
import android.view.View
|
||||
import androidx.core.view.isVisible
|
||||
import coil.clear
|
||||
import coil.dispose
|
||||
import coil.load
|
||||
import eu.davidea.viewholders.FlexibleViewHolder
|
||||
import eu.kanade.tachiyomi.R
|
||||
|
@ -39,7 +39,7 @@ class ExtensionHolder(view: View, val adapter: ExtensionAdapter) :
|
|||
else -> ""
|
||||
}.uppercase()
|
||||
|
||||
binding.icon.clear()
|
||||
binding.icon.dispose()
|
||||
if (extension is Extension.Available) {
|
||||
binding.icon.load(extension.iconUrl)
|
||||
} else if (extension is Extension.Installed) {
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
package eu.kanade.tachiyomi.ui.browse.migration.manga
|
||||
|
||||
import android.view.View
|
||||
import coil.clear
|
||||
import coil.loadAny
|
||||
import coil.dispose
|
||||
import coil.load
|
||||
import eu.davidea.viewholders.FlexibleViewHolder
|
||||
import eu.kanade.tachiyomi.databinding.SourceListItemBinding
|
||||
|
||||
|
@ -23,7 +23,7 @@ class MigrationMangaHolder(
|
|||
binding.title.text = item.manga.title
|
||||
|
||||
// Update the cover
|
||||
binding.thumbnail.clear()
|
||||
binding.thumbnail.loadAny(item.manga)
|
||||
binding.thumbnail.dispose()
|
||||
binding.thumbnail.load(item.manga)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
package eu.kanade.tachiyomi.ui.browse.source.browse
|
||||
|
||||
import androidx.core.view.isVisible
|
||||
import coil.clear
|
||||
import coil.dispose
|
||||
import coil.imageLoader
|
||||
import coil.request.ImageRequest
|
||||
import coil.transition.CrossfadeTransition
|
||||
|
@ -48,10 +48,10 @@ class SourceComfortableGridHolder(
|
|||
}
|
||||
|
||||
override fun setImage(manga: Manga) {
|
||||
binding.thumbnail.clear()
|
||||
binding.thumbnail.dispose()
|
||||
if (!manga.thumbnail_url.isNullOrEmpty()) {
|
||||
val crossfadeDuration = binding.root.context.imageLoader.defaults.transition.let {
|
||||
if (it is CrossfadeTransition) it.durationMillis else 0
|
||||
val crossfadeDuration = binding.root.context.imageLoader.defaults.transitionFactory.let {
|
||||
if (it is CrossfadeTransition.Factory) it.durationMillis else 0
|
||||
}
|
||||
val request = ImageRequest.Builder(binding.root.context)
|
||||
.data(manga)
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
package eu.kanade.tachiyomi.ui.browse.source.browse
|
||||
|
||||
import androidx.core.view.isVisible
|
||||
import coil.clear
|
||||
import coil.dispose
|
||||
import coil.imageLoader
|
||||
import coil.request.ImageRequest
|
||||
import coil.transition.CrossfadeTransition
|
||||
|
@ -48,10 +48,10 @@ class SourceCompactGridHolder(
|
|||
}
|
||||
|
||||
override fun setImage(manga: Manga) {
|
||||
binding.thumbnail.clear()
|
||||
binding.thumbnail.dispose()
|
||||
if (!manga.thumbnail_url.isNullOrEmpty()) {
|
||||
val crossfadeDuration = binding.root.context.imageLoader.defaults.transition.let {
|
||||
if (it is CrossfadeTransition) it.durationMillis else 0
|
||||
val crossfadeDuration = binding.root.context.imageLoader.defaults.transitionFactory.let {
|
||||
if (it is CrossfadeTransition.Factory) it.durationMillis else 0
|
||||
}
|
||||
val request = ImageRequest.Builder(binding.root.context)
|
||||
.data(manga)
|
||||
|
|
|
@ -2,8 +2,8 @@ package eu.kanade.tachiyomi.ui.browse.source.browse
|
|||
|
||||
import android.view.View
|
||||
import androidx.core.view.isVisible
|
||||
import coil.clear
|
||||
import coil.loadAny
|
||||
import coil.dispose
|
||||
import coil.load
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.coil.MangaCoverFetcher
|
||||
|
@ -50,9 +50,9 @@ class SourceListHolder(private val view: View, adapter: FlexibleAdapter<*>) :
|
|||
}
|
||||
|
||||
override fun setImage(manga: Manga) {
|
||||
binding.thumbnail.clear()
|
||||
binding.thumbnail.dispose()
|
||||
if (!manga.thumbnail_url.isNullOrEmpty()) {
|
||||
binding.thumbnail.loadAny(manga) {
|
||||
binding.thumbnail.load(manga) {
|
||||
setParameter(MangaCoverFetcher.USE_CUSTOM_COVER, false)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@ package eu.kanade.tachiyomi.ui.browse.source.globalsearch
|
|||
|
||||
import android.view.View
|
||||
import androidx.core.view.isVisible
|
||||
import coil.clear
|
||||
import coil.dispose
|
||||
import coil.imageLoader
|
||||
import coil.request.ImageRequest
|
||||
import coil.transition.CrossfadeTransition
|
||||
|
@ -53,10 +53,10 @@ class GlobalSearchCardHolder(view: View, adapter: GlobalSearchCardAdapter) :
|
|||
}
|
||||
|
||||
fun setImage(manga: Manga) {
|
||||
binding.cover.clear()
|
||||
binding.cover.dispose()
|
||||
if (!manga.thumbnail_url.isNullOrEmpty()) {
|
||||
val crossfadeDuration = itemView.context.imageLoader.defaults.transition.let {
|
||||
if (it is CrossfadeTransition) it.durationMillis else 0
|
||||
val crossfadeDuration = itemView.context.imageLoader.defaults.transitionFactory.let {
|
||||
if (it is CrossfadeTransition.Factory) it.durationMillis else 0
|
||||
}
|
||||
val request = ImageRequest.Builder(itemView.context)
|
||||
.data(manga)
|
||||
|
|
|
@ -2,11 +2,11 @@ package eu.kanade.tachiyomi.ui.library
|
|||
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import coil.clear
|
||||
import coil.dispose
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
import eu.davidea.flexibleadapter.items.IFlexible
|
||||
import eu.kanade.tachiyomi.databinding.SourceComfortableGridItemBinding
|
||||
import eu.kanade.tachiyomi.util.view.loadAnyAutoPause
|
||||
import eu.kanade.tachiyomi.util.view.loadAutoPause
|
||||
|
||||
/**
|
||||
* Class used to hold the displayed data of a manga in the library, like the cover or the title.
|
||||
|
@ -55,7 +55,7 @@ class LibraryComfortableGridHolder(
|
|||
binding.badges.localText.isVisible = item.isLocal
|
||||
|
||||
// Update the cover.
|
||||
binding.thumbnail.clear()
|
||||
binding.thumbnail.loadAnyAutoPause(item.manga)
|
||||
binding.thumbnail.dispose()
|
||||
binding.thumbnail.loadAutoPause(item.manga)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
package eu.kanade.tachiyomi.ui.library
|
||||
|
||||
import androidx.core.view.isVisible
|
||||
import coil.clear
|
||||
import coil.dispose
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
import eu.kanade.tachiyomi.databinding.SourceCompactGridItemBinding
|
||||
import eu.kanade.tachiyomi.util.view.loadAnyAutoPause
|
||||
import eu.kanade.tachiyomi.util.view.loadAutoPause
|
||||
|
||||
/**
|
||||
* Class used to hold the displayed data of a manga in the library, like the cover or the title.
|
||||
|
@ -54,11 +54,11 @@ class LibraryCompactGridHolder(
|
|||
binding.badges.localText.isVisible = item.isLocal
|
||||
|
||||
// Update the cover.
|
||||
binding.thumbnail.clear()
|
||||
binding.thumbnail.dispose()
|
||||
if (coverOnly) {
|
||||
// Cover only mode: Hides title text unless thumbnail is unavailable
|
||||
if (!item.manga.thumbnail_url.isNullOrEmpty()) {
|
||||
binding.thumbnail.loadAnyAutoPause(item.manga)
|
||||
binding.thumbnail.loadAutoPause(item.manga)
|
||||
binding.title.isVisible = false
|
||||
} else {
|
||||
binding.title.text = item.manga.title
|
||||
|
@ -66,7 +66,7 @@ class LibraryCompactGridHolder(
|
|||
}
|
||||
binding.thumbnail.foreground = null
|
||||
} else {
|
||||
binding.thumbnail.loadAnyAutoPause(item.manga)
|
||||
binding.thumbnail.loadAutoPause(item.manga)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,8 +2,8 @@ package eu.kanade.tachiyomi.ui.library
|
|||
|
||||
import android.view.View
|
||||
import androidx.core.view.isVisible
|
||||
import coil.clear
|
||||
import coil.loadAny
|
||||
import coil.dispose
|
||||
import coil.load
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
import eu.kanade.tachiyomi.databinding.SourceListItemBinding
|
||||
|
||||
|
@ -61,7 +61,7 @@ class LibraryListHolder(
|
|||
}
|
||||
|
||||
// Update the cover
|
||||
binding.thumbnail.clear()
|
||||
binding.thumbnail.loadAny(item.manga)
|
||||
binding.thumbnail.dispose()
|
||||
binding.thumbnail.load(item.manga)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -334,7 +334,7 @@ class MangaPresenter(
|
|||
* @return cover as Bitmap or null if there is no thumbnail cached with the memoryCacheKey
|
||||
*/
|
||||
private fun coverBitmapFromImageLoader(context: Context, memoryCacheKey: MemoryCache.Key): Bitmap? {
|
||||
return context.imageLoader.memoryCache[memoryCacheKey]
|
||||
return context.imageLoader.memoryCache?.get(memoryCacheKey)?.bitmap
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -19,7 +19,7 @@ import eu.kanade.tachiyomi.source.online.HttpSource
|
|||
import eu.kanade.tachiyomi.ui.base.controller.getMainAppBarHeight
|
||||
import eu.kanade.tachiyomi.ui.manga.MangaController
|
||||
import eu.kanade.tachiyomi.util.system.copyToClipboard
|
||||
import eu.kanade.tachiyomi.util.view.loadAnyAutoPause
|
||||
import eu.kanade.tachiyomi.util.view.loadAutoPause
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import reactivecircus.flowbinding.android.view.clicks
|
||||
|
@ -286,8 +286,8 @@ class MangaInfoHeaderAdapter(
|
|||
setFavoriteButtonState(manga.favorite)
|
||||
|
||||
// Set cover if changed.
|
||||
binding.backdrop.loadAnyAutoPause(manga)
|
||||
binding.mangaCover.loadAnyAutoPause(manga)
|
||||
binding.backdrop.loadAutoPause(manga)
|
||||
binding.mangaCover.loadAutoPause(manga)
|
||||
|
||||
// Manga info section
|
||||
binding.mangaSummarySection.setTags(manga.getGenres(), controller::performGenreSearch)
|
||||
|
|
|
@ -2,8 +2,8 @@ package eu.kanade.tachiyomi.ui.manga.track
|
|||
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import coil.clear
|
||||
import coil.loadAny
|
||||
import coil.dispose
|
||||
import coil.load
|
||||
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
||||
import eu.kanade.tachiyomi.databinding.TrackSearchItemBinding
|
||||
import java.util.Locale
|
||||
|
@ -20,9 +20,9 @@ class TrackSearchHolder(
|
|||
}
|
||||
|
||||
binding.trackSearchTitle.text = track.title
|
||||
binding.trackSearchCover.clear()
|
||||
binding.trackSearchCover.dispose()
|
||||
if (track.cover_url.isNotEmpty()) {
|
||||
binding.trackSearchCover.loadAny(track.cover_url)
|
||||
binding.trackSearchCover.load(track.cover_url)
|
||||
}
|
||||
|
||||
val hasStatus = track.publishing_status.isNotBlank()
|
||||
|
|
|
@ -17,7 +17,7 @@ import androidx.annotation.CallSuper
|
|||
import androidx.annotation.StyleRes
|
||||
import androidx.appcompat.widget.AppCompatImageView
|
||||
import androidx.core.view.isVisible
|
||||
import coil.clear
|
||||
import coil.dispose
|
||||
import coil.imageLoader
|
||||
import coil.request.CachePolicy
|
||||
import coil.request.ImageRequest
|
||||
|
@ -152,7 +152,7 @@ open class ReaderPageImageView @JvmOverloads constructor(
|
|||
fun recycle() = pageView?.let {
|
||||
when (it) {
|
||||
is SubsamplingScaleImageView -> it.recycle()
|
||||
is AppCompatImageView -> it.clear()
|
||||
is AppCompatImageView -> it.dispose()
|
||||
}
|
||||
it.isVisible = false
|
||||
}
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
package eu.kanade.tachiyomi.ui.recent.history
|
||||
|
||||
import android.view.View
|
||||
import coil.clear
|
||||
import coil.loadAny
|
||||
import coil.dispose
|
||||
import coil.load
|
||||
import eu.davidea.viewholders.FlexibleViewHolder
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.database.models.MangaChapterHistory
|
||||
|
@ -65,7 +65,7 @@ class HistoryHolder(
|
|||
}
|
||||
|
||||
// Set cover
|
||||
binding.cover.clear()
|
||||
binding.cover.loadAny(item.manga)
|
||||
binding.cover.dispose()
|
||||
binding.cover.load(item.manga)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,8 +2,8 @@ package eu.kanade.tachiyomi.ui.recent.updates
|
|||
|
||||
import android.view.View
|
||||
import androidx.core.view.isVisible
|
||||
import coil.clear
|
||||
import coil.loadAny
|
||||
import coil.dispose
|
||||
import coil.load
|
||||
import eu.kanade.tachiyomi.databinding.UpdatesItemBinding
|
||||
import eu.kanade.tachiyomi.source.LocalSource
|
||||
import eu.kanade.tachiyomi.ui.manga.chapter.base.BaseChapterHolder
|
||||
|
@ -58,7 +58,7 @@ class UpdatesHolder(private val view: View, private val adapter: UpdatesAdapter)
|
|||
binding.download.setState(item.status, item.progress)
|
||||
|
||||
// Set cover
|
||||
binding.mangaCover.clear()
|
||||
binding.mangaCover.loadAny(item.manga)
|
||||
binding.mangaCover.dispose()
|
||||
binding.mangaCover.load(item.manga)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,7 +8,7 @@ import androidx.annotation.DrawableRes
|
|||
import androidx.appcompat.content.res.AppCompatResources
|
||||
import coil.ImageLoader
|
||||
import coil.imageLoader
|
||||
import coil.loadAny
|
||||
import coil.load
|
||||
import coil.request.ImageRequest
|
||||
import coil.target.ImageViewTarget
|
||||
import eu.kanade.tachiyomi.util.system.animatorDurationScale
|
||||
|
@ -33,12 +33,13 @@ fun ImageView.setVectorCompat(@DrawableRes drawable: Int, @AttrRes tint: Int? =
|
|||
* and if the image is animated, this will also disable that animation
|
||||
* if [Context.animatorDurationScale] is 0
|
||||
*/
|
||||
fun ImageView.loadAnyAutoPause(
|
||||
fun ImageView.loadAutoPause(
|
||||
data: Any?,
|
||||
loader: ImageLoader = context.imageLoader,
|
||||
builder: ImageRequest.Builder.() -> Unit = {}
|
||||
) {
|
||||
this.loadAny(data, loader) {
|
||||
// Build the original request so we can add on our success listener
|
||||
load(data, loader) {
|
||||
// Build the original request so we can add on our success listener
|
||||
val originalBuild = apply(builder).build()
|
||||
listener(
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
aboutlib_version = "8.9.4"
|
||||
okhttp_version = "4.9.1"
|
||||
nucleus_version = "3.0.0"
|
||||
coil_version = "1.4.0"
|
||||
coil_version = "2.0.0-rc01"
|
||||
conductor_version = "3.1.2"
|
||||
flowbinding_version = "1.2.0"
|
||||
shizuku_version = "12.1.0"
|
||||
|
|
Reference in a new issue