Explorar el Código

Use Coil (#4870)

* Use Coil

* Remove coil-transformations lib

* Add MangaCoverFetcher

* Remove Glide

* MangaCoverFetcher: Allow skipping custom cover usage

* Adjust coil caching policy for some non-library items

* Allow coil to use RGB565 only on low ram devices

* Fix image loading progress view not showing

a

* Increase coil crossfade duration

Same as default glide duration

* Add back request clearing
Ivan Iskandar hace 3 años
padre
commit
93e6136795
Se han modificado 39 ficheros con 493 adiciones y 798 borrados
  1. 5 5
      app/build.gradle.kts
  2. 28 1
      app/src/main/java/eu/kanade/tachiyomi/App.kt
  3. 8 0
      app/src/main/java/eu/kanade/tachiyomi/data/cache/CoverCache.kt
  4. 25 0
      app/src/main/java/eu/kanade/tachiyomi/data/coil/ByteBufferFetcher.kt
  5. 172 0
      app/src/main/java/eu/kanade/tachiyomi/data/coil/MangaCoverFetcher.kt
  6. 0 60
      app/src/main/java/eu/kanade/tachiyomi/data/glide/FileFetcher.kt
  7. 0 25
      app/src/main/java/eu/kanade/tachiyomi/data/glide/LibraryMangaCustomCoverFetcher.kt
  8. 0 86
      app/src/main/java/eu/kanade/tachiyomi/data/glide/LibraryMangaUrlFetcher.kt
  9. 0 15
      app/src/main/java/eu/kanade/tachiyomi/data/glide/MangaThumbnail.kt
  10. 0 134
      app/src/main/java/eu/kanade/tachiyomi/data/glide/MangaThumbnailModelLoader.kt
  11. 0 72
      app/src/main/java/eu/kanade/tachiyomi/data/glide/PassthroughModelLoader.kt
  12. 0 55
      app/src/main/java/eu/kanade/tachiyomi/data/glide/TachiGlideModule.kt
  13. 19 23
      app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateNotifier.kt
  14. 1 0
      app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateService.kt
  15. 23 18
      app/src/main/java/eu/kanade/tachiyomi/network/NetworkHelper.kt
  16. 4 5
      app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/ExtensionHolder.kt
  17. 8 16
      app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/manga/MigrationMangaHolder.kt
  18. 17 10
      app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/SourceComfortableGridHolder.kt
  19. 17 10
      app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/SourceGridHolder.kt
  20. 12 17
      app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/SourceListHolder.kt
  21. 17 11
      app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/GlobalSearchCardHolder.kt
  22. 4 10
      app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryComfortableGridHolder.kt
  23. 4 10
      app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryCompactGridHolder.kt
  24. 8 16
      app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryListHolder.kt
  25. 2 0
      app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaPresenter.kt
  26. 3 16
      app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaInfoHeaderAdapter.kt
  27. 4 8
      app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackSearchAdapter.kt
  28. 1 0
      app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderPresenter.kt
  29. 19 17
      app/src/main/java/eu/kanade/tachiyomi/ui/reader/SaveImageNotifier.kt
  30. 21 40
      app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerPageHolder.kt
  31. 23 41
      app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonPageHolder.kt
  32. 8 15
      app/src/main/java/eu/kanade/tachiyomi/ui/recent/history/HistoryHolder.kt
  33. 8 16
      app/src/main/java/eu/kanade/tachiyomi/ui/recent/updates/UpdatesHolder.kt
  34. 26 46
      app/src/main/java/eu/kanade/tachiyomi/widget/StateImageViewTarget.kt
  35. 2 0
      app/src/main/res/layout/manga_info_header.xml
  36. 1 0
      app/src/main/res/layout/source_comfortable_grid_item.xml
  37. 1 0
      app/src/main/res/layout/source_compact_grid_item.xml
  38. 1 0
      app/src/main/res/layout/source_list_item.xml
  39. 1 0
      app/src/main/res/layout/updates_item.xml

+ 5 - 5
app/build.gradle.kts

@@ -198,10 +198,9 @@ dependencies {
     implementation("com.github.inorichi.injekt:injekt-core:65b0440")
 
     // Image library
-    val glideVersion = "4.12.0"
-    implementation("com.github.bumptech.glide:glide:$glideVersion")
-    implementation("com.github.bumptech.glide:okhttp3-integration:$glideVersion")
-    kapt("com.github.bumptech.glide:compiler:$glideVersion")
+    val coilVersion = "1.2.0"
+    implementation("io.coil-kt:coil:$coilVersion")
+    implementation("io.coil-kt:coil-gif:$coilVersion")
 
     implementation("com.github.tachiyomiorg:subsampling-scale-image-view:547d9c0")
 
@@ -278,7 +277,8 @@ tasks {
             "-Xuse-experimental=kotlinx.coroutines.FlowPreview",
             "-Xuse-experimental=kotlinx.coroutines.ExperimentalCoroutinesApi",
             "-Xuse-experimental=kotlinx.coroutines.InternalCoroutinesApi",
-            "-Xuse-experimental=kotlinx.serialization.ExperimentalSerializationApi"
+            "-Xuse-experimental=kotlinx.serialization.ExperimentalSerializationApi",
+            "-Xuse-experimental=coil.annotation.ExperimentalCoilApi",
         )
     }
 

+ 28 - 1
app/src/main/java/eu/kanade/tachiyomi/App.kt

@@ -1,16 +1,25 @@
 package eu.kanade.tachiyomi
 
+import android.app.ActivityManager
 import android.app.Application
 import android.content.Context
 import android.content.res.Configuration
 import android.os.Build
+import androidx.core.content.getSystemService
 import androidx.lifecycle.Lifecycle
 import androidx.lifecycle.LifecycleObserver
 import androidx.lifecycle.OnLifecycleEvent
 import androidx.lifecycle.ProcessLifecycleOwner
 import androidx.multidex.MultiDex
+import coil.ImageLoader
+import coil.ImageLoaderFactory
+import coil.decode.GifDecoder
+import coil.decode.ImageDecoderDecoder
+import eu.kanade.tachiyomi.data.coil.ByteBufferFetcher
+import eu.kanade.tachiyomi.data.coil.MangaCoverFetcher
 import eu.kanade.tachiyomi.data.notification.Notifications
 import eu.kanade.tachiyomi.data.preference.PreferencesHelper
+import eu.kanade.tachiyomi.network.NetworkHelper
 import eu.kanade.tachiyomi.ui.security.SecureActivityDelegate
 import eu.kanade.tachiyomi.util.system.LocaleHelper
 import org.acra.ACRA
@@ -20,6 +29,7 @@ import org.acra.sender.HttpSender
 import org.conscrypt.Conscrypt
 import timber.log.Timber
 import uy.kohesive.injekt.Injekt
+import uy.kohesive.injekt.api.get
 import uy.kohesive.injekt.injectLazy
 import java.security.Security
 
@@ -31,7 +41,7 @@ import java.security.Security
     uri = BuildConfig.ACRA_URI,
     httpMethod = HttpSender.Method.PUT
 )
-open class App : Application(), LifecycleObserver {
+open class App : Application(), LifecycleObserver, ImageLoaderFactory {
 
     private val preferences: PreferencesHelper by injectLazy()
 
@@ -67,6 +77,23 @@ open class App : Application(), LifecycleObserver {
         LocaleHelper.updateConfiguration(this, newConfig, true)
     }
 
+    override fun newImageLoader(): ImageLoader {
+        return ImageLoader.Builder(this).apply {
+            componentRegistry {
+                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
+                    add(ImageDecoderDecoder(this@App))
+                } else {
+                    add(GifDecoder())
+                }
+                add(ByteBufferFetcher())
+                add(MangaCoverFetcher())
+            }
+            okHttpClient(Injekt.get<NetworkHelper>().coilClient)
+            crossfade(300)
+            allowRgb565(getSystemService<ActivityManager>()!!.isLowRamDevice)
+        }.build()
+    }
+
     @OnLifecycleEvent(Lifecycle.Event.ON_STOP)
     @Suppress("unused")
     fun onAppBackgrounded() {

+ 8 - 0
app/src/main/java/eu/kanade/tachiyomi/data/cache/CoverCache.kt

@@ -1,6 +1,7 @@
 package eu.kanade.tachiyomi.data.cache
 
 import android.content.Context
+import coil.imageLoader
 import eu.kanade.tachiyomi.data.database.models.Manga
 import eu.kanade.tachiyomi.util.storage.DiskUtil
 import java.io.File
@@ -99,6 +100,13 @@ class CoverCache(private val context: Context) {
         }
     }
 
+    /**
+     * Clear coil's memory cache.
+     */
+    fun clearMemoryCache() {
+        context.imageLoader.memoryCache.clear()
+    }
+
     private fun getCacheDir(dir: String): File {
         return context.getExternalFilesDir(dir)
             ?: File(context.filesDir, dir).also { it.mkdirs() }

+ 25 - 0
app/src/main/java/eu/kanade/tachiyomi/data/coil/ByteBufferFetcher.kt

@@ -0,0 +1,25 @@
+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
+}

+ 172 - 0
app/src/main/java/eu/kanade/tachiyomi/data/coil/MangaCoverFetcher.kt

@@ -0,0 +1,172 @@
+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.network.HttpException
+import coil.request.get
+import coil.size.Size
+import eu.kanade.tachiyomi.data.cache.CoverCache
+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
+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 uy.kohesive.injekt.injectLazy
+import java.io.File
+import java.util.Date
+
+/**
+ * Coil component that fetches [Manga] cover while using the cached file in disk when available.
+ *
+ * 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
+
+    override fun key(data: Manga): String? {
+        if (data.thumbnail_url.isNullOrBlank()) return null
+        return data.thumbnail_url!!
+    }
+
+    override suspend fun fetch(pool: BitmapPool, data: Manga, size: Size, options: Options): FetchResult {
+        // Use custom cover if exists
+        val useCustomCover = options.parameters[USE_CUSTOM_COVER] as? Boolean ?: true
+        val customCoverFile = coverCache.getCustomCoverFile(data)
+        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)
+            null -> error("Invalid image")
+        }
+    }
+
+    private suspend fun httpLoader(manga: Manga, options: Options): FetchResult {
+        val coverFile = coverCache.getCoverFile(manga) ?: error("No cover specified")
+
+        // Use previously cached cover if exist
+        if (coverFile.exists() && options.diskCachePolicy.readEnabled) {
+            if (!manga.favorite) {
+                coverFile.setLastModified(Date().time)
+            }
+            return fileLoader(coverFile)
+        }
+
+        val (response, body) = awaitGetCall(manga, options)
+        if (!response.isSuccessful) {
+            body.close()
+            throw HttpException(response)
+        }
+
+        // Write to disk for future use
+        if (options.diskCachePolicy.writeEnabled) {
+            response.peekBody(Long.MAX_VALUE).source().use { input ->
+                val tmpFile = File(coverFile.absolutePath + "_tmp")
+                tmpFile.parentFile?.mkdirs()
+                tmpFile.sink().buffer().use { output ->
+                    output.writeAll(input)
+                }
+                if (coverFile.exists()) {
+                    coverFile.delete()
+                }
+                tmpFile.renameTo(coverFile)
+            }
+        }
+
+        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 client = source?.client ?: defaultClient
+
+        val newClient = client.newBuilder().build()
+
+        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()
+
+        return newClient.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(),
+            mimeType = "image/*",
+            dataSource = DataSource.DISK
+        )
+    }
+
+    private fun getResourceType(cover: String?): Type? {
+        return when {
+            cover.isNullOrEmpty() -> null
+            cover.startsWith("http") || cover.startsWith("Custom-", true) -> Type.URL
+            cover.startsWith("/") || cover.startsWith("file://") -> Type.File
+            else -> null
+        }
+    }
+
+    private enum class Type {
+        File, URL
+    }
+
+    companion object {
+        const val USE_CUSTOM_COVER = "use_custom_cover"
+
+        private val CACHE_CONTROL_FORCE_NETWORK_NO_CACHE = CacheControl.Builder().noCache().noStore().build()
+        private val CACHE_CONTROL_NO_NETWORK_NO_CACHE = CacheControl.Builder().noCache().onlyIfCached().build()
+    }
+}

+ 0 - 60
app/src/main/java/eu/kanade/tachiyomi/data/glide/FileFetcher.kt

@@ -1,60 +0,0 @@
-package eu.kanade.tachiyomi.data.glide
-
-import android.content.ContentValues.TAG
-import android.util.Log
-import com.bumptech.glide.Priority
-import com.bumptech.glide.load.DataSource
-import com.bumptech.glide.load.data.DataFetcher
-import timber.log.Timber
-import java.io.File
-import java.io.FileInputStream
-import java.io.FileNotFoundException
-import java.io.IOException
-import java.io.InputStream
-
-open class FileFetcher(private val filePath: String = "") : DataFetcher<InputStream> {
-
-    private var data: InputStream? = null
-
-    override fun loadData(priority: Priority, callback: DataFetcher.DataCallback<in InputStream>) {
-        loadFromFile(callback)
-    }
-
-    private fun loadFromFile(callback: DataFetcher.DataCallback<in InputStream>) {
-        loadFromFile(File(filePath), callback)
-    }
-
-    protected fun loadFromFile(file: File, callback: DataFetcher.DataCallback<in InputStream>) {
-        try {
-            data = FileInputStream(file)
-        } catch (e: FileNotFoundException) {
-            if (Log.isLoggable(TAG, Log.DEBUG)) {
-                Timber.d(e, "Failed to open file")
-            }
-            callback.onLoadFailed(e)
-            return
-        }
-
-        callback.onDataReady(data)
-    }
-
-    override fun cleanup() {
-        try {
-            data?.close()
-        } catch (e: IOException) {
-            // Ignored.
-        }
-    }
-
-    override fun cancel() {
-        // Do nothing.
-    }
-
-    override fun getDataClass(): Class<InputStream> {
-        return InputStream::class.java
-    }
-
-    override fun getDataSource(): DataSource {
-        return DataSource.LOCAL
-    }
-}

+ 0 - 25
app/src/main/java/eu/kanade/tachiyomi/data/glide/LibraryMangaCustomCoverFetcher.kt

@@ -1,25 +0,0 @@
-package eu.kanade.tachiyomi.data.glide
-
-import com.bumptech.glide.Priority
-import com.bumptech.glide.load.data.DataFetcher
-import eu.kanade.tachiyomi.data.cache.CoverCache
-import eu.kanade.tachiyomi.data.database.models.Manga
-import java.io.File
-import java.io.InputStream
-import java.lang.Exception
-
-open class LibraryMangaCustomCoverFetcher(
-    private val manga: Manga,
-    private val coverCache: CoverCache
-) : FileFetcher() {
-
-    override fun loadData(priority: Priority, callback: DataFetcher.DataCallback<in InputStream>) {
-        getCustomCoverFile()?.let {
-            loadFromFile(it, callback)
-        } ?: callback.onLoadFailed(Exception("Custom cover file not found"))
-    }
-
-    protected fun getCustomCoverFile(): File? {
-        return coverCache.getCustomCoverFile(manga).takeIf { it.exists() }
-    }
-}

+ 0 - 86
app/src/main/java/eu/kanade/tachiyomi/data/glide/LibraryMangaUrlFetcher.kt

@@ -1,86 +0,0 @@
-package eu.kanade.tachiyomi.data.glide
-
-import com.bumptech.glide.Priority
-import com.bumptech.glide.load.data.DataFetcher
-import eu.kanade.tachiyomi.data.cache.CoverCache
-import eu.kanade.tachiyomi.data.database.models.Manga
-import java.io.File
-import java.io.FileNotFoundException
-import java.io.InputStream
-
-/**
- * A [DataFetcher] for loading a cover of a library manga.
- * It tries to load the cover from our custom cache, and if it's not found, it fallbacks to network
- * and copies the result to the cache.
- *
- * @param networkFetcher the network fetcher for this cover.
- * @param manga the manga of the cover to load.
- * @param file the file where this cover should be. It may exists or not.
- */
-class LibraryMangaUrlFetcher(
-    private val networkFetcher: DataFetcher<InputStream>,
-    private val manga: Manga,
-    private val coverCache: CoverCache
-) : LibraryMangaCustomCoverFetcher(manga, coverCache) {
-
-    override fun loadData(priority: Priority, callback: DataFetcher.DataCallback<in InputStream>) {
-        getCustomCoverFile()?.let {
-            loadFromFile(it, callback)
-            return
-        }
-
-        val cover = coverCache.getCoverFile(manga)
-        if (cover == null) {
-            callback.onLoadFailed(Exception("Null thumbnail url"))
-            return
-        }
-
-        if (!cover.exists()) {
-            networkFetcher.loadData(
-                priority,
-                object : DataFetcher.DataCallback<InputStream> {
-                    override fun onDataReady(data: InputStream?) {
-                        if (data != null) {
-                            val tmpFile = File(cover.path + ".tmp")
-                            try {
-                                // Retrieve destination stream, create parent folders if needed.
-                                val output = try {
-                                    tmpFile.outputStream()
-                                } catch (e: FileNotFoundException) {
-                                    tmpFile.parentFile!!.mkdirs()
-                                    tmpFile.outputStream()
-                                }
-
-                                // Copy the file and rename to the original.
-                                data.use { output.use { data.copyTo(output) } }
-                                tmpFile.renameTo(cover)
-                                loadFromFile(cover, callback)
-                            } catch (e: Exception) {
-                                tmpFile.delete()
-                                callback.onLoadFailed(e)
-                            }
-                        } else {
-                            callback.onLoadFailed(Exception("Null data"))
-                        }
-                    }
-
-                    override fun onLoadFailed(e: Exception) {
-                        callback.onLoadFailed(e)
-                    }
-                }
-            )
-        } else {
-            loadFromFile(cover, callback)
-        }
-    }
-
-    override fun cleanup() {
-        super.cleanup()
-        networkFetcher.cleanup()
-    }
-
-    override fun cancel() {
-        super.cancel()
-        networkFetcher.cancel()
-    }
-}

+ 0 - 15
app/src/main/java/eu/kanade/tachiyomi/data/glide/MangaThumbnail.kt

@@ -1,15 +0,0 @@
-package eu.kanade.tachiyomi.data.glide
-
-import com.bumptech.glide.load.Key
-import eu.kanade.tachiyomi.data.database.models.Manga
-import java.security.MessageDigest
-
-data class MangaThumbnail(val manga: Manga, val coverLastModified: Long) : Key {
-    val key = manga.url + coverLastModified
-
-    override fun updateDiskCacheKey(messageDigest: MessageDigest) {
-        messageDigest.update(key.toByteArray(Key.CHARSET))
-    }
-}
-
-fun Manga.toMangaThumbnail() = MangaThumbnail(this, cover_last_modified)

+ 0 - 134
app/src/main/java/eu/kanade/tachiyomi/data/glide/MangaThumbnailModelLoader.kt

@@ -1,134 +0,0 @@
-package eu.kanade.tachiyomi.data.glide
-
-import com.bumptech.glide.integration.okhttp3.OkHttpStreamFetcher
-import com.bumptech.glide.load.Options
-import com.bumptech.glide.load.model.GlideUrl
-import com.bumptech.glide.load.model.Headers
-import com.bumptech.glide.load.model.LazyHeaders
-import com.bumptech.glide.load.model.ModelLoader
-import com.bumptech.glide.load.model.ModelLoaderFactory
-import com.bumptech.glide.load.model.MultiModelLoaderFactory
-import eu.kanade.tachiyomi.data.cache.CoverCache
-import eu.kanade.tachiyomi.data.database.models.Manga
-import eu.kanade.tachiyomi.network.NetworkHelper
-import eu.kanade.tachiyomi.source.SourceManager
-import eu.kanade.tachiyomi.source.online.HttpSource
-import eu.kanade.tachiyomi.util.isLocal
-import uy.kohesive.injekt.Injekt
-import uy.kohesive.injekt.api.get
-import uy.kohesive.injekt.injectLazy
-import java.io.InputStream
-
-/**
- * A class for loading a cover associated with a [Manga] that can be present in our own cache.
- * Coupled with [LibraryMangaUrlFetcher], this class allows to implement the following flow:
- *
- * - Check in RAM LRU.
- * - Check in disk LRU.
- * - Check in this module.
- * - Fetch from the network connection.
- *
- * @param context the application context.
- */
-class MangaThumbnailModelLoader : ModelLoader<MangaThumbnail, InputStream> {
-
-    /**
-     * Cover cache where persistent covers are stored.
-     */
-    private val coverCache: CoverCache by injectLazy()
-
-    /**
-     * Source manager.
-     */
-    private val sourceManager: SourceManager by injectLazy()
-
-    /**
-     * Default network client.
-     */
-    private val defaultClient = Injekt.get<NetworkHelper>().client
-
-    /**
-     * Map where request headers are stored for a source.
-     */
-    private val cachedHeaders = hashMapOf<Long, LazyHeaders>()
-
-    /**
-     * Factory class for creating [MangaThumbnailModelLoader] instances.
-     */
-    class Factory : ModelLoaderFactory<MangaThumbnail, InputStream> {
-
-        override fun build(multiFactory: MultiModelLoaderFactory): ModelLoader<MangaThumbnail, InputStream> {
-            return MangaThumbnailModelLoader()
-        }
-
-        override fun teardown() {}
-    }
-
-    override fun handles(model: MangaThumbnail): Boolean {
-        return true
-    }
-
-    /**
-     * Returns a fetcher for the given manga or null if the url is empty.
-     *
-     * @param mangaThumbnail the model.
-     * @param width the width of the view where the resource will be loaded.
-     * @param height the height of the view where the resource will be loaded.
-     */
-    override fun buildLoadData(
-        mangaThumbnail: MangaThumbnail,
-        width: Int,
-        height: Int,
-        options: Options
-    ): ModelLoader.LoadData<InputStream>? {
-        val manga = mangaThumbnail.manga
-        val url = manga.thumbnail_url
-
-        if (url.isNullOrEmpty()) {
-            return if (!manga.favorite || manga.isLocal()) {
-                null
-            } else {
-                ModelLoader.LoadData(mangaThumbnail, LibraryMangaCustomCoverFetcher(manga, coverCache))
-            }
-        }
-
-        if (url.startsWith("http", true)) {
-            val source = sourceManager.get(manga.source) as? HttpSource
-            val glideUrl = GlideUrl(url, getHeaders(manga, source))
-
-            // Get the resource fetcher for this request url.
-            val networkFetcher = OkHttpStreamFetcher(source?.client ?: defaultClient, glideUrl)
-
-            if (!manga.favorite) {
-                return ModelLoader.LoadData(glideUrl, networkFetcher)
-            }
-
-            val libraryFetcher = LibraryMangaUrlFetcher(networkFetcher, manga, coverCache)
-
-            // Return an instance of the fetcher providing the needed elements.
-            return ModelLoader.LoadData(mangaThumbnail, libraryFetcher)
-        } else {
-            // Return an instance of the fetcher providing the needed elements.
-            return ModelLoader.LoadData(mangaThumbnail, FileFetcher(url.removePrefix("file://")))
-        }
-    }
-
-    /**
-     * Returns the request headers for a source copying its OkHttp headers and caching them.
-     *
-     * @param manga the model.
-     */
-    private fun getHeaders(manga: Manga, source: HttpSource?): Headers {
-        if (source == null) return LazyHeaders.DEFAULT
-
-        return cachedHeaders.getOrPut(manga.source) {
-            LazyHeaders.Builder().apply {
-                val nullStr: String? = null
-                setHeader("User-Agent", nullStr)
-                for ((key, value) in source.headers.toMultimap()) {
-                    addHeader(key, value[0])
-                }
-            }.build()
-        }
-    }
-}

+ 0 - 72
app/src/main/java/eu/kanade/tachiyomi/data/glide/PassthroughModelLoader.kt

@@ -1,72 +0,0 @@
-package eu.kanade.tachiyomi.data.glide
-
-import com.bumptech.glide.Priority
-import com.bumptech.glide.load.DataSource
-import com.bumptech.glide.load.Options
-import com.bumptech.glide.load.data.DataFetcher
-import com.bumptech.glide.load.model.ModelLoader
-import com.bumptech.glide.load.model.ModelLoaderFactory
-import com.bumptech.glide.load.model.MultiModelLoaderFactory
-import com.bumptech.glide.signature.ObjectKey
-import java.io.IOException
-import java.io.InputStream
-
-class PassthroughModelLoader : ModelLoader<InputStream, InputStream> {
-
-    override fun buildLoadData(
-        model: InputStream,
-        width: Int,
-        height: Int,
-        options: Options
-    ): ModelLoader.LoadData<InputStream>? {
-        return ModelLoader.LoadData(ObjectKey(model), Fetcher(model))
-    }
-
-    override fun handles(model: InputStream): Boolean {
-        return true
-    }
-
-    class Fetcher(private val stream: InputStream) : DataFetcher<InputStream> {
-
-        override fun getDataClass(): Class<InputStream> {
-            return InputStream::class.java
-        }
-
-        override fun cleanup() {
-            try {
-                stream.close()
-            } catch (e: IOException) {
-                // Do nothing
-            }
-        }
-
-        override fun getDataSource(): DataSource {
-            return DataSource.LOCAL
-        }
-
-        override fun cancel() {
-            // Do nothing
-        }
-
-        override fun loadData(
-            priority: Priority,
-            callback: DataFetcher.DataCallback<in InputStream>
-        ) {
-            callback.onDataReady(stream)
-        }
-    }
-
-    /**
-     * Factory class for creating [PassthroughModelLoader] instances.
-     */
-    class Factory : ModelLoaderFactory<InputStream, InputStream> {
-
-        override fun build(
-            multiFactory: MultiModelLoaderFactory
-        ): ModelLoader<InputStream, InputStream> {
-            return PassthroughModelLoader()
-        }
-
-        override fun teardown() {}
-    }
-}

+ 0 - 55
app/src/main/java/eu/kanade/tachiyomi/data/glide/TachiGlideModule.kt

@@ -1,55 +0,0 @@
-package eu.kanade.tachiyomi.data.glide
-
-import android.content.Context
-import android.graphics.drawable.Drawable
-import com.bumptech.glide.Glide
-import com.bumptech.glide.GlideBuilder
-import com.bumptech.glide.Registry
-import com.bumptech.glide.annotation.GlideModule
-import com.bumptech.glide.integration.okhttp3.OkHttpUrlLoader
-import com.bumptech.glide.load.DecodeFormat
-import com.bumptech.glide.load.engine.cache.InternalCacheDiskCacheFactory
-import com.bumptech.glide.load.model.GlideUrl
-import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions
-import com.bumptech.glide.module.AppGlideModule
-import com.bumptech.glide.request.RequestOptions
-import eu.kanade.tachiyomi.network.NetworkHelper
-import uy.kohesive.injekt.Injekt
-import uy.kohesive.injekt.api.get
-import java.io.InputStream
-
-/**
- * Class used to update Glide module settings
- */
-@GlideModule
-class TachiGlideModule : AppGlideModule() {
-
-    override fun applyOptions(context: Context, builder: GlideBuilder) {
-        builder.setDiskCache(InternalCacheDiskCacheFactory(context, 50 * 1024 * 1024))
-        builder.setDefaultRequestOptions(RequestOptions().format(DecodeFormat.PREFER_RGB_565))
-        builder.setDefaultTransitionOptions(
-            Drawable::class.java,
-            DrawableTransitionOptions.withCrossFade()
-        )
-    }
-
-    override fun registerComponents(context: Context, glide: Glide, registry: Registry) {
-        val networkFactory = OkHttpUrlLoader.Factory(Injekt.get<NetworkHelper>().client)
-
-        registry.replace(
-            GlideUrl::class.java,
-            InputStream::class.java,
-            networkFactory
-        )
-        registry.append(
-            MangaThumbnail::class.java,
-            InputStream::class.java,
-            MangaThumbnailModelLoader.Factory()
-        )
-        registry.append(
-            InputStream::class.java,
-            InputStream::class.java,
-            PassthroughModelLoader.Factory()
-        )
-    }
-}

+ 19 - 23
app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateNotifier.kt

@@ -6,19 +6,22 @@ import android.content.Context
 import android.content.Intent
 import android.graphics.Bitmap
 import android.graphics.BitmapFactory
+import android.graphics.drawable.BitmapDrawable
 import android.net.Uri
 import androidx.core.app.NotificationCompat
 import androidx.core.app.NotificationManagerCompat
-import com.bumptech.glide.Glide
+import coil.imageLoader
+import coil.request.ImageRequest
+import coil.transform.CircleCropTransformation
 import eu.kanade.tachiyomi.R
 import eu.kanade.tachiyomi.data.database.models.Chapter
 import eu.kanade.tachiyomi.data.database.models.Manga
-import eu.kanade.tachiyomi.data.glide.toMangaThumbnail
 import eu.kanade.tachiyomi.data.notification.NotificationReceiver
 import eu.kanade.tachiyomi.data.notification.Notifications
 import eu.kanade.tachiyomi.data.preference.PreferencesHelper
 import eu.kanade.tachiyomi.ui.main.MainActivity
 import eu.kanade.tachiyomi.util.lang.chop
+import eu.kanade.tachiyomi.util.lang.launchUI
 import eu.kanade.tachiyomi.util.system.notification
 import eu.kanade.tachiyomi.util.system.notificationBuilder
 import eu.kanade.tachiyomi.util.system.notificationManager
@@ -165,14 +168,17 @@ class LibraryUpdateNotifier(private val context: Context) {
 
             // Per-manga notification
             if (!preferences.hideNotificationContent()) {
-                updates.forEach { (manga, chapters) ->
-                    notify(manga.id.hashCode(), createNewChaptersNotification(manga, chapters))
+                launchUI {
+                    updates.forEach { (manga, chapters) ->
+                        notify(manga.id.hashCode(), createNewChaptersNotification(manga, chapters))
+                    }
                 }
             }
         }
     }
 
-    private fun createNewChaptersNotification(manga: Manga, chapters: Array<Chapter>): Notification {
+    private suspend fun createNewChaptersNotification(manga: Manga, chapters: Array<Chapter>): Notification {
+        val icon = getMangaIcon(manga)
         return context.notification(Notifications.CHANNEL_NEW_CHAPTERS) {
             setContentTitle(manga.title)
 
@@ -182,7 +188,6 @@ class LibraryUpdateNotifier(private val context: Context) {
 
             setSmallIcon(R.drawable.ic_tachi)
 
-            val icon = getMangaIcon(manga)
             if (icon != null) {
                 setLargeIcon(icon)
             }
@@ -226,23 +231,14 @@ class LibraryUpdateNotifier(private val context: Context) {
         context.notificationManager.cancel(Notifications.ID_LIBRARY_PROGRESS)
     }
 
-    private fun getMangaIcon(manga: Manga): Bitmap? {
-        return try {
-            Glide.with(context)
-                .asBitmap()
-                .load(manga.toMangaThumbnail())
-                .dontTransform()
-                .centerCrop()
-                .circleCrop()
-                .override(
-                    NOTIF_ICON_SIZE,
-                    NOTIF_ICON_SIZE
-                )
-                .submit()
-                .get()
-        } catch (e: Exception) {
-            null
-        }
+    private suspend fun getMangaIcon(manga: Manga): Bitmap? {
+        val request = ImageRequest.Builder(context)
+            .data(manga)
+            .transformations(CircleCropTransformation())
+            .size(NOTIF_ICON_SIZE)
+            .build()
+        val drawable = context.imageLoader.execute(request).drawable
+        return (drawable as? BitmapDrawable)?.bitmap
     }
 
     private fun getNewChaptersDescription(chapters: Array<Chapter>): String {

+ 1 - 0
app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateService.kt

@@ -386,6 +386,7 @@ class LibraryUpdateService(
             }
         }
 
+        coverCache.clearMemoryCache()
         notifier.cancelProgressNotification()
     }
 

+ 23 - 18
app/src/main/java/eu/kanade/tachiyomi/network/NetworkHelper.kt

@@ -1,6 +1,7 @@
 package eu.kanade.tachiyomi.network
 
 import android.content.Context
+import coil.util.CoilUtils
 import eu.kanade.tachiyomi.BuildConfig
 import eu.kanade.tachiyomi.data.preference.PreferencesHelper
 import okhttp3.Cache
@@ -20,28 +21,32 @@ class NetworkHelper(context: Context) {
 
     val cookieManager = AndroidCookieJar()
 
-    val client by lazy {
-        val builder = OkHttpClient.Builder()
-            .cookieJar(cookieManager)
-            .cache(Cache(cacheDir, cacheSize))
-            .connectTimeout(30, TimeUnit.SECONDS)
-            .readTimeout(30, TimeUnit.SECONDS)
-            .addInterceptor(UserAgentInterceptor())
-
-        if (BuildConfig.DEBUG) {
-            val httpLoggingInterceptor = HttpLoggingInterceptor().apply {
-                level = HttpLoggingInterceptor.Level.HEADERS
+    private val baseClientBuilder: OkHttpClient.Builder
+        get() {
+            val builder = OkHttpClient.Builder()
+                .cookieJar(cookieManager)
+                .connectTimeout(30, TimeUnit.SECONDS)
+                .readTimeout(30, TimeUnit.SECONDS)
+                .addInterceptor(UserAgentInterceptor())
+
+            if (BuildConfig.DEBUG) {
+                val httpLoggingInterceptor = HttpLoggingInterceptor().apply {
+                    level = HttpLoggingInterceptor.Level.HEADERS
+                }
+                builder.addInterceptor(httpLoggingInterceptor)
+            }
+
+            when (preferences.dohProvider()) {
+                PREF_DOH_CLOUDFLARE -> builder.dohCloudflare()
+                PREF_DOH_GOOGLE -> builder.dohGoogle()
             }
-            builder.addInterceptor(httpLoggingInterceptor)
-        }
 
-        when (preferences.dohProvider()) {
-            PREF_DOH_CLOUDFLARE -> builder.dohCloudflare()
-            PREF_DOH_GOOGLE -> builder.dohGoogle()
+            return builder
         }
 
-        builder.build()
-    }
+    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()

+ 4 - 5
app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/ExtensionHolder.kt

@@ -1,9 +1,10 @@
 package eu.kanade.tachiyomi.ui.browse.extension
 
 import android.view.View
+import coil.clear
+import coil.load
 import eu.davidea.viewholders.FlexibleViewHolder
 import eu.kanade.tachiyomi.R
-import eu.kanade.tachiyomi.data.glide.GlideApp
 import eu.kanade.tachiyomi.data.preference.PreferencesHelper
 import eu.kanade.tachiyomi.databinding.ExtensionCardItemBinding
 import eu.kanade.tachiyomi.extension.model.Extension
@@ -41,11 +42,9 @@ class ExtensionHolder(view: View, val adapter: ExtensionAdapter) :
             else -> ""
         }.toUpperCase()
 
-        GlideApp.with(itemView.context).clear(binding.image)
+        binding.image.clear()
         if (extension is Extension.Available) {
-            GlideApp.with(itemView.context)
-                .load(extension.iconUrl)
-                .into(binding.image)
+            binding.image.load(extension.iconUrl)
         } else {
             extension.getApplicationIcon(itemView.context)?.let { binding.image.setImageDrawable(it) }
         }

+ 8 - 16
app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/manga/MigrationMangaHolder.kt

@@ -1,14 +1,11 @@
 package eu.kanade.tachiyomi.ui.browse.migration.manga
 
 import android.view.View
-import com.bumptech.glide.load.engine.DiskCacheStrategy
-import com.bumptech.glide.load.resource.bitmap.CenterCrop
-import com.bumptech.glide.load.resource.bitmap.RoundedCorners
-import com.bumptech.glide.request.RequestOptions
+import coil.clear
+import coil.loadAny
+import coil.transform.RoundedCornersTransformation
 import eu.davidea.viewholders.FlexibleViewHolder
 import eu.kanade.tachiyomi.R
-import eu.kanade.tachiyomi.data.glide.GlideApp
-import eu.kanade.tachiyomi.data.glide.toMangaThumbnail
 import eu.kanade.tachiyomi.databinding.SourceListItemBinding
 
 class MigrationMangaHolder(
@@ -28,15 +25,10 @@ class MigrationMangaHolder(
         binding.title.text = item.manga.title
 
         // Update the cover.
-        GlideApp.with(itemView.context).clear(binding.thumbnail)
-
-        val radius = itemView.context.resources.getDimensionPixelSize(R.dimen.card_radius)
-        val requestOptions = RequestOptions().transform(CenterCrop(), RoundedCorners(radius))
-        GlideApp.with(itemView.context)
-            .load(item.manga.toMangaThumbnail())
-            .diskCacheStrategy(DiskCacheStrategy.RESOURCE)
-            .apply(requestOptions)
-            .dontAnimate()
-            .into(binding.thumbnail)
+        val radius = itemView.context.resources.getDimension(R.dimen.card_radius)
+        binding.thumbnail.clear()
+        binding.thumbnail.loadAny(item.manga) {
+            transformations(RoundedCornersTransformation(radius))
+        }
     }
 }

+ 17 - 10
app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/SourceComfortableGridHolder.kt

@@ -1,11 +1,14 @@
 package eu.kanade.tachiyomi.ui.browse.source.browse
 
 import android.view.View
-import com.bumptech.glide.load.engine.DiskCacheStrategy
+import coil.clear
+import coil.imageLoader
+import coil.request.CachePolicy
+import coil.request.ImageRequest
+import coil.transition.CrossfadeTransition
 import eu.davidea.flexibleadapter.FlexibleAdapter
+import eu.kanade.tachiyomi.data.coil.MangaCoverFetcher
 import eu.kanade.tachiyomi.data.database.models.Manga
-import eu.kanade.tachiyomi.data.glide.GlideApp
-import eu.kanade.tachiyomi.data.glide.toMangaThumbnail
 import eu.kanade.tachiyomi.databinding.SourceComfortableGridItemBinding
 import eu.kanade.tachiyomi.widget.StateImageViewTarget
 
@@ -42,14 +45,18 @@ class SourceComfortableGridHolder(private val view: View, private val adapter: F
         // For rounded corners
         binding.card.clipToOutline = true
 
-        GlideApp.with(view.context).clear(binding.thumbnail)
+        binding.thumbnail.clear()
         if (!manga.thumbnail_url.isNullOrEmpty()) {
-            GlideApp.with(view.context)
-                .load(manga.toMangaThumbnail())
-                .diskCacheStrategy(DiskCacheStrategy.DATA)
-                .centerCrop()
-                .placeholder(android.R.color.transparent)
-                .into(StateImageViewTarget(binding.thumbnail, binding.progress))
+            val crossfadeDuration = view.context.imageLoader.defaults.transition.let {
+                if (it is CrossfadeTransition) it.durationMillis else 0
+            }
+            val request = ImageRequest.Builder(view.context)
+                .data(manga)
+                .setParameter(MangaCoverFetcher.USE_CUSTOM_COVER, false)
+                .diskCachePolicy(CachePolicy.DISABLED)
+                .target(StateImageViewTarget(binding.thumbnail, binding.progress, crossfadeDuration))
+                .build()
+            itemView.context.imageLoader.enqueue(request)
         }
     }
 }

+ 17 - 10
app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/SourceGridHolder.kt

@@ -1,11 +1,14 @@
 package eu.kanade.tachiyomi.ui.browse.source.browse
 
 import android.view.View
-import com.bumptech.glide.load.engine.DiskCacheStrategy
+import coil.clear
+import coil.imageLoader
+import coil.request.CachePolicy
+import coil.request.ImageRequest
+import coil.transition.CrossfadeTransition
 import eu.davidea.flexibleadapter.FlexibleAdapter
+import eu.kanade.tachiyomi.data.coil.MangaCoverFetcher
 import eu.kanade.tachiyomi.data.database.models.Manga
-import eu.kanade.tachiyomi.data.glide.GlideApp
-import eu.kanade.tachiyomi.data.glide.toMangaThumbnail
 import eu.kanade.tachiyomi.databinding.SourceComfortableGridItemBinding
 import eu.kanade.tachiyomi.widget.StateImageViewTarget
 
@@ -42,14 +45,18 @@ open class SourceGridHolder(private val view: View, private val adapter: Flexibl
         // For rounded corners
         binding.card.clipToOutline = true
 
-        GlideApp.with(view.context).clear(binding.thumbnail)
+        binding.thumbnail.clear()
         if (!manga.thumbnail_url.isNullOrEmpty()) {
-            GlideApp.with(view.context)
-                .load(manga.toMangaThumbnail())
-                .diskCacheStrategy(DiskCacheStrategy.DATA)
-                .centerCrop()
-                .placeholder(android.R.color.transparent)
-                .into(StateImageViewTarget(binding.thumbnail, binding.progress))
+            val crossfadeDuration = view.context.imageLoader.defaults.transition.let {
+                if (it is CrossfadeTransition) it.durationMillis else 0
+            }
+            val request = ImageRequest.Builder(view.context)
+                .data(manga)
+                .setParameter(MangaCoverFetcher.USE_CUSTOM_COVER, false)
+                .diskCachePolicy(CachePolicy.DISABLED)
+                .target(StateImageViewTarget(binding.thumbnail, binding.progress, crossfadeDuration))
+                .build()
+            itemView.context.imageLoader.enqueue(request)
         }
     }
 }

+ 12 - 17
app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/SourceListHolder.kt

@@ -1,15 +1,14 @@
 package eu.kanade.tachiyomi.ui.browse.source.browse
 
 import android.view.View
-import com.bumptech.glide.load.engine.DiskCacheStrategy
-import com.bumptech.glide.load.resource.bitmap.CenterCrop
-import com.bumptech.glide.load.resource.bitmap.RoundedCorners
-import com.bumptech.glide.request.RequestOptions
+import coil.clear
+import coil.loadAny
+import coil.request.CachePolicy
+import coil.transform.RoundedCornersTransformation
 import eu.davidea.flexibleadapter.FlexibleAdapter
 import eu.kanade.tachiyomi.R
+import eu.kanade.tachiyomi.data.coil.MangaCoverFetcher
 import eu.kanade.tachiyomi.data.database.models.Manga
-import eu.kanade.tachiyomi.data.glide.GlideApp
-import eu.kanade.tachiyomi.data.glide.toMangaThumbnail
 import eu.kanade.tachiyomi.databinding.SourceListItemBinding
 import eu.kanade.tachiyomi.util.system.getResourceColor
 
@@ -46,18 +45,14 @@ class SourceListHolder(private val view: View, adapter: FlexibleAdapter<*>) :
     }
 
     override fun setImage(manga: Manga) {
-        GlideApp.with(view.context).clear(binding.thumbnail)
-
+        binding.thumbnail.clear()
         if (!manga.thumbnail_url.isNullOrEmpty()) {
-            val radius = view.context.resources.getDimensionPixelSize(R.dimen.card_radius)
-            val requestOptions = RequestOptions().transform(CenterCrop(), RoundedCorners(radius))
-            GlideApp.with(view.context)
-                .load(manga.toMangaThumbnail())
-                .diskCacheStrategy(DiskCacheStrategy.DATA)
-                .apply(requestOptions)
-                .dontAnimate()
-                .placeholder(android.R.color.transparent)
-                .into(binding.thumbnail)
+            val radius = view.context.resources.getDimension(R.dimen.card_radius)
+            binding.thumbnail.loadAny(manga) {
+                setParameter(MangaCoverFetcher.USE_CUSTOM_COVER, false)
+                transformations(RoundedCornersTransformation(radius))
+                diskCachePolicy(CachePolicy.DISABLED)
+            }
         }
     }
 }

+ 17 - 11
app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/GlobalSearchCardHolder.kt

@@ -1,11 +1,14 @@
 package eu.kanade.tachiyomi.ui.browse.source.globalsearch
 
 import android.view.View
-import com.bumptech.glide.load.engine.DiskCacheStrategy
+import coil.clear
+import coil.imageLoader
+import coil.request.CachePolicy
+import coil.request.ImageRequest
+import coil.transition.CrossfadeTransition
 import eu.davidea.viewholders.FlexibleViewHolder
+import eu.kanade.tachiyomi.data.coil.MangaCoverFetcher
 import eu.kanade.tachiyomi.data.database.models.Manga
-import eu.kanade.tachiyomi.data.glide.GlideApp
-import eu.kanade.tachiyomi.data.glide.toMangaThumbnail
 import eu.kanade.tachiyomi.databinding.GlobalSearchControllerCardItemBinding
 import eu.kanade.tachiyomi.widget.StateImageViewTarget
 
@@ -42,15 +45,18 @@ class GlobalSearchCardHolder(view: View, adapter: GlobalSearchCardAdapter) :
     }
 
     fun setImage(manga: Manga) {
-        GlideApp.with(itemView.context).clear(binding.cover)
+        binding.cover.clear()
         if (!manga.thumbnail_url.isNullOrEmpty()) {
-            GlideApp.with(itemView.context)
-                .load(manga.toMangaThumbnail())
-                .diskCacheStrategy(DiskCacheStrategy.DATA)
-                .centerCrop()
-                .skipMemoryCache(true)
-                .placeholder(android.R.color.transparent)
-                .into(StateImageViewTarget(binding.cover, binding.progress))
+            val crossfadeDuration = itemView.context.imageLoader.defaults.transition.let {
+                if (it is CrossfadeTransition) it.durationMillis else 0
+            }
+            val request = ImageRequest.Builder(itemView.context)
+                .data(manga)
+                .setParameter(MangaCoverFetcher.USE_CUSTOM_COVER, false)
+                .diskCachePolicy(CachePolicy.DISABLED)
+                .target(StateImageViewTarget(binding.cover, binding.progress, crossfadeDuration))
+                .build()
+            itemView.context.imageLoader.enqueue(request)
         }
     }
 }

+ 4 - 10
app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryComfortableGridHolder.kt

@@ -3,11 +3,10 @@ package eu.kanade.tachiyomi.ui.library
 import android.view.View
 import androidx.core.view.isVisible
 import androidx.recyclerview.widget.RecyclerView
-import com.bumptech.glide.load.engine.DiskCacheStrategy
+import coil.clear
+import coil.loadAny
 import eu.davidea.flexibleadapter.FlexibleAdapter
 import eu.davidea.flexibleadapter.items.IFlexible
-import eu.kanade.tachiyomi.data.glide.GlideApp
-import eu.kanade.tachiyomi.data.glide.toMangaThumbnail
 import eu.kanade.tachiyomi.databinding.SourceComfortableGridItemBinding
 import eu.kanade.tachiyomi.util.isLocal
 
@@ -57,12 +56,7 @@ class LibraryComfortableGridHolder(
         binding.card.clipToOutline = true
 
         // Update the cover.
-        GlideApp.with(view.context).clear(binding.thumbnail)
-        GlideApp.with(view.context)
-            .load(item.manga.toMangaThumbnail())
-            .diskCacheStrategy(DiskCacheStrategy.RESOURCE)
-            .centerCrop()
-            .dontAnimate()
-            .into(binding.thumbnail)
+        binding.thumbnail.clear()
+        binding.thumbnail.loadAny(item.manga)
     }
 }

+ 4 - 10
app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryCompactGridHolder.kt

@@ -2,10 +2,9 @@ package eu.kanade.tachiyomi.ui.library
 
 import android.view.View
 import androidx.core.view.isVisible
-import com.bumptech.glide.load.engine.DiskCacheStrategy
+import coil.clear
+import coil.loadAny
 import eu.davidea.flexibleadapter.FlexibleAdapter
-import eu.kanade.tachiyomi.data.glide.GlideApp
-import eu.kanade.tachiyomi.data.glide.toMangaThumbnail
 import eu.kanade.tachiyomi.databinding.SourceCompactGridItemBinding
 import eu.kanade.tachiyomi.util.isLocal
 
@@ -55,12 +54,7 @@ open class LibraryCompactGridHolder(
         binding.card.clipToOutline = true
 
         // Update the cover.
-        GlideApp.with(view.context).clear(binding.thumbnail)
-        GlideApp.with(view.context)
-            .load(item.manga.toMangaThumbnail())
-            .diskCacheStrategy(DiskCacheStrategy.RESOURCE)
-            .centerCrop()
-            .dontAnimate()
-            .into(binding.thumbnail)
+        binding.thumbnail.clear()
+        binding.thumbnail.loadAny(item.manga)
     }
 }

+ 8 - 16
app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryListHolder.kt

@@ -2,14 +2,11 @@ package eu.kanade.tachiyomi.ui.library
 
 import android.view.View
 import androidx.core.view.isVisible
-import com.bumptech.glide.load.engine.DiskCacheStrategy
-import com.bumptech.glide.load.resource.bitmap.CenterCrop
-import com.bumptech.glide.load.resource.bitmap.RoundedCorners
-import com.bumptech.glide.request.RequestOptions
+import coil.clear
+import coil.loadAny
+import coil.transform.RoundedCornersTransformation
 import eu.davidea.flexibleadapter.FlexibleAdapter
 import eu.kanade.tachiyomi.R
-import eu.kanade.tachiyomi.data.glide.GlideApp
-import eu.kanade.tachiyomi.data.glide.toMangaThumbnail
 import eu.kanade.tachiyomi.databinding.SourceListItemBinding
 import eu.kanade.tachiyomi.util.isLocal
 
@@ -62,15 +59,10 @@ class LibraryListHolder(
         }
 
         // Update the cover.
-        GlideApp.with(itemView.context).clear(binding.thumbnail)
-
-        val radius = view.context.resources.getDimensionPixelSize(R.dimen.card_radius)
-        val requestOptions = RequestOptions().transform(CenterCrop(), RoundedCorners(radius))
-        GlideApp.with(itemView.context)
-            .load(item.manga.toMangaThumbnail())
-            .diskCacheStrategy(DiskCacheStrategy.RESOURCE)
-            .apply(requestOptions)
-            .dontAnimate()
-            .into(binding.thumbnail)
+        val radius = view.context.resources.getDimension(R.dimen.card_radius)
+        binding.thumbnail.clear()
+        binding.thumbnail.loadAny(item.manga) {
+            transformations(RoundedCornersTransformation(radius))
+        }
     }
 }

+ 2 - 0
app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaPresenter.kt

@@ -284,6 +284,7 @@ class MangaPresenter(
                     } else if (manga.favorite) {
                         coverCache.setCustomCoverToCache(manga, it)
                         manga.updateCoverLastModified(db)
+                        coverCache.clearMemoryCache()
                     }
                 }
             }
@@ -300,6 +301,7 @@ class MangaPresenter(
             .fromCallable {
                 coverCache.deleteCustomCover(manga)
                 manga.updateCoverLastModified(db)
+                coverCache.clearMemoryCache()
             }
             .subscribeOn(Schedulers.io())
             .observeOn(AndroidSchedulers.mainThread())

+ 3 - 16
app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaInfoHeaderAdapter.kt

@@ -5,12 +5,9 @@ import android.view.View
 import android.view.ViewGroup
 import androidx.core.view.isVisible
 import androidx.recyclerview.widget.RecyclerView
-import com.bumptech.glide.load.engine.DiskCacheStrategy
+import coil.loadAny
 import eu.kanade.tachiyomi.R
 import eu.kanade.tachiyomi.data.database.models.Manga
-import eu.kanade.tachiyomi.data.glide.GlideApp
-import eu.kanade.tachiyomi.data.glide.MangaThumbnail
-import eu.kanade.tachiyomi.data.glide.toMangaThumbnail
 import eu.kanade.tachiyomi.data.track.TrackManager
 import eu.kanade.tachiyomi.databinding.MangaInfoHeaderBinding
 import eu.kanade.tachiyomi.source.Source
@@ -44,7 +41,6 @@ class MangaInfoHeaderAdapter(
     private lateinit var binding: MangaInfoHeaderBinding
 
     private var initialLoad: Boolean = true
-    private var currentMangaThumbnail: MangaThumbnail? = null
 
     override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): HeaderViewHolder {
         binding = MangaInfoHeaderBinding.inflate(LayoutInflater.from(parent.context), parent, false)
@@ -246,17 +242,8 @@ class MangaInfoHeaderAdapter(
             setFavoriteButtonState(manga.favorite)
 
             // Set cover if changed.
-            val mangaThumbnail = manga.toMangaThumbnail()
-            if (mangaThumbnail != currentMangaThumbnail) {
-                currentMangaThumbnail = mangaThumbnail
-                listOf(binding.mangaCover, binding.backdrop)
-                    .forEach {
-                        GlideApp.with(view.context)
-                            .load(mangaThumbnail)
-                            .diskCacheStrategy(DiskCacheStrategy.RESOURCE)
-                            .centerCrop()
-                            .into(it)
-                    }
+            listOf(binding.mangaCover, binding.backdrop).forEach {
+                it.loadAny(manga)
             }
 
             // Manga info section

+ 4 - 8
app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackSearchAdapter.kt

@@ -5,9 +5,9 @@ import android.view.View
 import android.view.ViewGroup
 import android.widget.ArrayAdapter
 import androidx.core.view.isVisible
-import com.bumptech.glide.load.engine.DiskCacheStrategy
+import coil.clear
+import coil.load
 import eu.kanade.tachiyomi.R
-import eu.kanade.tachiyomi.data.glide.GlideApp
 import eu.kanade.tachiyomi.data.track.model.TrackSearch
 import eu.kanade.tachiyomi.databinding.TrackSearchItemBinding
 import eu.kanade.tachiyomi.util.view.inflate
@@ -46,13 +46,9 @@ class TrackSearchAdapter(context: Context) :
         fun onSetValues(track: TrackSearch) {
             binding.trackSearchTitle.text = track.title
             binding.trackSearchSummary.text = track.summary
-            GlideApp.with(view.context).clear(binding.trackSearchCover)
+            binding.trackSearchCover.clear()
             if (track.cover_url.isNotEmpty()) {
-                GlideApp.with(view.context)
-                    .load(track.cover_url)
-                    .diskCacheStrategy(DiskCacheStrategy.RESOURCE)
-                    .centerCrop()
-                    .into(binding.trackSearchCover)
+                binding.trackSearchCover.load(track.cover_url)
             }
 
             val hasStatus = track.publishing_status.isNotBlank()

+ 1 - 0
app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderPresenter.kt

@@ -624,6 +624,7 @@ class ReaderPresenter(
                     if (manga.favorite) {
                         coverCache.setCustomCoverToCache(manga, stream())
                         manga.updateCoverLastModified(db)
+                        coverCache.clearMemoryCache()
                         SetAsCoverResult.Success
                     } else {
                         SetAsCoverResult.AddToLibraryFirst

+ 19 - 17
app/src/main/java/eu/kanade/tachiyomi/ui/reader/SaveImageNotifier.kt

@@ -2,10 +2,12 @@ package eu.kanade.tachiyomi.ui.reader
 
 import android.content.Context
 import android.graphics.Bitmap
+import android.graphics.drawable.BitmapDrawable
 import androidx.core.app.NotificationCompat
-import com.bumptech.glide.load.engine.DiskCacheStrategy
+import coil.imageLoader
+import coil.request.CachePolicy
+import coil.request.ImageRequest
 import eu.kanade.tachiyomi.R
-import eu.kanade.tachiyomi.data.glide.GlideApp
 import eu.kanade.tachiyomi.data.notification.NotificationHandler
 import eu.kanade.tachiyomi.data.notification.NotificationReceiver
 import eu.kanade.tachiyomi.data.notification.Notifications
@@ -30,25 +32,25 @@ class SaveImageNotifier(private val context: Context) {
         get() = Notifications.ID_DOWNLOAD_IMAGE
 
     /**
-     * Called when image download/copy is complete. This method must be called in a background
-     * thread.
+     * Called when image download/copy is complete.
      *
      * @param file image file containing downloaded page image.
      */
     fun onComplete(file: File) {
-        val bitmap = GlideApp.with(context)
-            .asBitmap()
-            .load(file)
-            .diskCacheStrategy(DiskCacheStrategy.NONE)
-            .skipMemoryCache(true)
-            .submit(720, 1280)
-            .get()
-
-        if (bitmap != null) {
-            showCompleteNotification(file, bitmap)
-        } else {
-            onError(null)
-        }
+        val request = ImageRequest.Builder(context)
+            .data(file)
+            .memoryCachePolicy(CachePolicy.DISABLED)
+            .size(720, 1280)
+            .target(
+                onSuccess = { result ->
+                    showCompleteNotification(file, (result as BitmapDrawable).bitmap)
+                },
+                onError = {
+                    onError(null)
+                }
+            )
+            .build()
+        context.imageLoader.enqueue(request)
     }
 
     private fun showCompleteNotification(file: File, image: Bitmap) {

+ 21 - 40
app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerPageHolder.kt

@@ -2,7 +2,7 @@ package eu.kanade.tachiyomi.ui.reader.viewer.pager
 
 import android.annotation.SuppressLint
 import android.graphics.PointF
-import android.graphics.drawable.Drawable
+import android.graphics.drawable.Animatable
 import android.view.GestureDetector
 import android.view.Gravity
 import android.view.MotionEvent
@@ -14,19 +14,13 @@ import android.widget.ImageView
 import android.widget.LinearLayout
 import android.widget.TextView
 import androidx.core.view.isVisible
-import com.bumptech.glide.load.DataSource
-import com.bumptech.glide.load.engine.DiskCacheStrategy
-import com.bumptech.glide.load.engine.GlideException
-import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions
-import com.bumptech.glide.load.resource.gif.GifDrawable
-import com.bumptech.glide.request.RequestListener
-import com.bumptech.glide.request.target.Target
-import com.bumptech.glide.request.transition.NoTransition
+import coil.imageLoader
+import coil.request.CachePolicy
+import coil.request.ImageRequest
 import com.davemorrissey.labs.subscaleview.ImageSource
 import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
 import com.github.chrisbanes.photoview.PhotoView
 import eu.kanade.tachiyomi.R
-import eu.kanade.tachiyomi.data.glide.GlideApp
 import eu.kanade.tachiyomi.source.model.Page
 import eu.kanade.tachiyomi.ui.reader.model.InsertPage
 import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
@@ -41,6 +35,7 @@ import rx.Subscription
 import rx.android.schedulers.AndroidSchedulers
 import rx.schedulers.Schedulers
 import java.io.InputStream
+import java.nio.ByteBuffer
 import java.util.concurrent.TimeUnit
 
 /**
@@ -480,38 +475,24 @@ class PagerPageHolder(
      * Extension method to set a [stream] into this ImageView.
      */
     private fun ImageView.setImage(stream: InputStream) {
-        GlideApp.with(this)
-            .load(stream)
-            .skipMemoryCache(true)
-            .diskCacheStrategy(DiskCacheStrategy.NONE)
-            .transition(DrawableTransitionOptions.with(NoTransition.getFactory()))
-            .listener(
-                object : RequestListener<Drawable> {
-                    override fun onLoadFailed(
-                        e: GlideException?,
-                        model: Any?,
-                        target: Target<Drawable>?,
-                        isFirstResource: Boolean
-                    ): Boolean {
-                        onImageDecodeError()
-                        return false
-                    }
-
-                    override fun onResourceReady(
-                        resource: Drawable?,
-                        model: Any?,
-                        target: Target<Drawable>?,
-                        dataSource: DataSource?,
-                        isFirstResource: Boolean
-                    ): Boolean {
-                        if (resource is GifDrawable) {
-                            resource.setLoopCount(GifDrawable.LOOP_INTRINSIC)
-                        }
-                        onImageDecoded()
-                        return false
+        val request = ImageRequest.Builder(context)
+            .data(ByteBuffer.wrap(stream.readBytes()))
+            .memoryCachePolicy(CachePolicy.DISABLED)
+            .diskCachePolicy(CachePolicy.DISABLED)
+            .target(
+                onSuccess = { result ->
+                    if (result is Animatable) {
+                        result.start()
                     }
+                    setImageDrawable(result)
+                    onImageDecoded()
+                },
+                onError = {
+                    onImageDecodeError()
                 }
             )
-            .into(this)
+            .crossfade(false)
+            .build()
+        context.imageLoader.enqueue(request)
     }
 }

+ 23 - 41
app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonPageHolder.kt

@@ -2,7 +2,7 @@ package eu.kanade.tachiyomi.ui.reader.viewer.webtoon
 
 import android.annotation.SuppressLint
 import android.content.res.Resources
-import android.graphics.drawable.Drawable
+import android.graphics.drawable.Animatable
 import android.view.Gravity
 import android.view.ViewGroup
 import android.view.ViewGroup.LayoutParams.MATCH_PARENT
@@ -14,18 +14,13 @@ import android.widget.TextView
 import androidx.appcompat.widget.AppCompatButton
 import androidx.appcompat.widget.AppCompatImageView
 import androidx.core.view.isVisible
-import com.bumptech.glide.load.DataSource
-import com.bumptech.glide.load.engine.DiskCacheStrategy
-import com.bumptech.glide.load.engine.GlideException
-import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions
-import com.bumptech.glide.load.resource.gif.GifDrawable
-import com.bumptech.glide.request.RequestListener
-import com.bumptech.glide.request.target.Target
-import com.bumptech.glide.request.transition.NoTransition
+import coil.clear
+import coil.imageLoader
+import coil.request.CachePolicy
+import coil.request.ImageRequest
 import com.davemorrissey.labs.subscaleview.ImageSource
 import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
 import eu.kanade.tachiyomi.R
-import eu.kanade.tachiyomi.data.glide.GlideApp
 import eu.kanade.tachiyomi.source.model.Page
 import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
 import eu.kanade.tachiyomi.ui.reader.viewer.ReaderProgressBar
@@ -37,6 +32,7 @@ import rx.Subscription
 import rx.android.schedulers.AndroidSchedulers
 import rx.schedulers.Schedulers
 import java.io.InputStream
+import java.nio.ByteBuffer
 import java.util.concurrent.TimeUnit
 
 /**
@@ -146,7 +142,7 @@ class WebtoonPageHolder(
         removeDecodeErrorLayout()
         subsamplingImageView?.recycle()
         subsamplingImageView?.isVisible = false
-        imageView?.let { GlideApp.with(frame).clear(it) }
+        imageView?.clear()
         imageView?.isVisible = false
         progressBar.setProgress(0)
     }
@@ -512,38 +508,24 @@ class WebtoonPageHolder(
      * Extension method to set a [stream] into this ImageView.
      */
     private fun ImageView.setImage(stream: InputStream) {
-        GlideApp.with(this)
-            .load(stream)
-            .skipMemoryCache(true)
-            .diskCacheStrategy(DiskCacheStrategy.NONE)
-            .transition(DrawableTransitionOptions.with(NoTransition.getFactory()))
-            .listener(
-                object : RequestListener<Drawable> {
-                    override fun onLoadFailed(
-                        e: GlideException?,
-                        model: Any?,
-                        target: Target<Drawable>?,
-                        isFirstResource: Boolean
-                    ): Boolean {
-                        onImageDecodeError()
-                        return false
-                    }
-
-                    override fun onResourceReady(
-                        resource: Drawable?,
-                        model: Any?,
-                        target: Target<Drawable>?,
-                        dataSource: DataSource?,
-                        isFirstResource: Boolean
-                    ): Boolean {
-                        if (resource is GifDrawable) {
-                            resource.setLoopCount(GifDrawable.LOOP_INTRINSIC)
-                        }
-                        onImageDecoded()
-                        return false
+        val request = ImageRequest.Builder(context)
+            .data(ByteBuffer.wrap(stream.readBytes()))
+            .memoryCachePolicy(CachePolicy.DISABLED)
+            .diskCachePolicy(CachePolicy.DISABLED)
+            .target(
+                onSuccess = { result ->
+                    if (result is Animatable) {
+                        result.start()
                     }
+                    setImageDrawable(result)
+                    onImageDecoded()
+                },
+                onError = {
+                    onImageDecodeError()
                 }
             )
-            .into(this)
+            .crossfade(false)
+            .build()
+        context.imageLoader.enqueue(request)
     }
 }

+ 8 - 15
app/src/main/java/eu/kanade/tachiyomi/ui/recent/history/HistoryHolder.kt

@@ -1,15 +1,12 @@
 package eu.kanade.tachiyomi.ui.recent.history
 
 import android.view.View
-import com.bumptech.glide.load.engine.DiskCacheStrategy
-import com.bumptech.glide.load.resource.bitmap.CenterCrop
-import com.bumptech.glide.load.resource.bitmap.RoundedCorners
-import com.bumptech.glide.request.RequestOptions
+import coil.clear
+import coil.loadAny
+import coil.transform.RoundedCornersTransformation
 import eu.davidea.viewholders.FlexibleViewHolder
 import eu.kanade.tachiyomi.R
 import eu.kanade.tachiyomi.data.database.models.MangaChapterHistory
-import eu.kanade.tachiyomi.data.glide.GlideApp
-import eu.kanade.tachiyomi.data.glide.toMangaThumbnail
 import eu.kanade.tachiyomi.databinding.HistoryItemBinding
 import eu.kanade.tachiyomi.util.lang.toTimestampString
 import java.util.Date
@@ -68,15 +65,11 @@ class HistoryHolder(
             binding.mangaSubtitle.text = Date(history.last_read).toTimestampString()
         }
 
-        val radius = itemView.context.resources.getDimensionPixelSize(R.dimen.card_radius)
-        val requestOptions = RequestOptions().transform(CenterCrop(), RoundedCorners(radius))
-
         // Set cover
-        GlideApp.with(itemView.context).clear(binding.cover)
-        GlideApp.with(itemView.context)
-            .load(manga.toMangaThumbnail())
-            .diskCacheStrategy(DiskCacheStrategy.RESOURCE)
-            .apply(requestOptions)
-            .into(binding.cover)
+        val radius = itemView.context.resources.getDimension(R.dimen.card_radius)
+        binding.cover.clear()
+        binding.cover.loadAny(item.manga) {
+            transformations(RoundedCornersTransformation(radius))
+        }
     }
 }

+ 8 - 16
app/src/main/java/eu/kanade/tachiyomi/ui/recent/updates/UpdatesHolder.kt

@@ -2,13 +2,10 @@ package eu.kanade.tachiyomi.ui.recent.updates
 
 import android.view.View
 import androidx.core.view.isVisible
-import com.bumptech.glide.load.engine.DiskCacheStrategy
-import com.bumptech.glide.load.resource.bitmap.CenterCrop
-import com.bumptech.glide.load.resource.bitmap.RoundedCorners
-import com.bumptech.glide.request.RequestOptions
+import coil.clear
+import coil.loadAny
+import coil.transform.RoundedCornersTransformation
 import eu.kanade.tachiyomi.R
-import eu.kanade.tachiyomi.data.glide.GlideApp
-import eu.kanade.tachiyomi.data.glide.toMangaThumbnail
 import eu.kanade.tachiyomi.databinding.UpdatesItemBinding
 import eu.kanade.tachiyomi.source.LocalSource
 import eu.kanade.tachiyomi.ui.manga.chapter.base.BaseChapterHolder
@@ -58,15 +55,10 @@ class UpdatesHolder(private val view: View, private val adapter: UpdatesAdapter)
         binding.download.setState(item.status, item.progress)
 
         // Set cover
-        GlideApp.with(itemView.context).clear(binding.mangaCover)
-
-        val radius = itemView.context.resources.getDimensionPixelSize(R.dimen.card_radius)
-        val requestOptions = RequestOptions().transform(CenterCrop(), RoundedCorners(radius))
-        GlideApp.with(itemView.context)
-            .load(item.manga.toMangaThumbnail())
-            .diskCacheStrategy(DiskCacheStrategy.RESOURCE)
-            .apply(requestOptions)
-            .dontAnimate()
-            .into(binding.mangaCover)
+        val radius = itemView.context.resources.getDimension(R.dimen.card_radius)
+        binding.mangaCover.clear()
+        binding.mangaCover.loadAny(item.manga) {
+            transformations(RoundedCornersTransformation(radius))
+        }
     }
 }

+ 26 - 46
app/src/main/java/eu/kanade/tachiyomi/widget/StateImageViewTarget.kt

@@ -3,61 +3,41 @@ package eu.kanade.tachiyomi.widget
 import android.graphics.drawable.Drawable
 import android.view.View
 import android.widget.ImageView
-import android.widget.ImageView.ScaleType
-import androidx.appcompat.content.res.AppCompatResources
 import androidx.core.view.isVisible
-import com.bumptech.glide.request.target.ImageViewTarget
-import com.bumptech.glide.request.transition.Transition
-import eu.kanade.tachiyomi.R
-import eu.kanade.tachiyomi.util.system.getResourceColor
+import coil.drawable.CrossfadeDrawable
+import coil.target.ImageViewTarget
 
 /**
- * A glide target to display an image with an optional view to show while loading and a configurable
- * error drawable.
+ * A Coil target to display an image with an optional view to show while loading.
  *
- * @param view the view where the image will be loaded
- * @param progress an optional view to show when the image is loading.
- * @param errorDrawableRes the error drawable resource to show.
- * @param errorScaleType the scale type for the error drawable, [ScaleType.CENTER] by default.
+ * @param target the view where the image will be loaded
+ * @param progress the view to show when the image is loading.
+ * @param crossfadeDuration duration in millisecond to crossfade the result drawable
  */
 class StateImageViewTarget(
-    view: ImageView,
-    val progress: View? = null,
-    private val errorDrawableRes: Int = R.drawable.ic_broken_image_grey_24dp,
-    private val errorScaleType: ScaleType = ScaleType.CENTER
-) : ImageViewTarget<Drawable>(view) {
-
-    private var resource: Drawable? = null
-
-    private val imageScaleType = view.scaleType
-
-    override fun setResource(resource: Drawable?) {
-        view.setImageDrawable(resource)
-    }
-
-    override fun onLoadStarted(placeholder: Drawable?) {
-        progress?.isVisible = true
-        super.onLoadStarted(placeholder)
-    }
-
-    override fun onLoadFailed(errorDrawable: Drawable?) {
-        progress?.isVisible = false
-        view.scaleType = errorScaleType
-
-        val vector = AppCompatResources.getDrawable(view.context, errorDrawableRes)
-        vector?.setTint(view.context.getResourceColor(R.attr.colorOnBackground, 0.38f))
-        view.setImageDrawable(vector)
+    private val target: ImageView,
+    private val progress: View,
+    private val crossfadeDuration: Int = 0
+) : ImageViewTarget(target) {
+    override fun onStart(placeholder: Drawable?) {
+        progress.isVisible = true
     }
 
-    override fun onLoadCleared(placeholder: Drawable?) {
-        progress?.isVisible = false
-        super.onLoadCleared(placeholder)
+    override fun onSuccess(result: Drawable) {
+        progress.isVisible = false
+        if (crossfadeDuration > 0) {
+            val crossfadeResult = CrossfadeDrawable(target.drawable, result, durationMillis = crossfadeDuration)
+            target.setImageDrawable(crossfadeResult)
+            crossfadeResult.start()
+        } else {
+            target.setImageDrawable(result)
+        }
     }
 
-    override fun onResourceReady(resource: Drawable, transition: Transition<in Drawable>?) {
-        progress?.isVisible = false
-        view.scaleType = imageScaleType
-        super.onResourceReady(resource, transition)
-        this.resource = resource
+    override fun onError(error: Drawable?) {
+        progress.isVisible = false
+        if (error != null) {
+            target.setImageDrawable(error)
+        }
     }
 }

+ 2 - 0
app/src/main/res/layout/manga_info_header.xml

@@ -17,6 +17,7 @@
             android:layout_height="0dp"
             android:layout_marginBottom="44dp"
             android:alpha="0.2"
+            android:scaleType="centerCrop"
             app:layout_constraintBottom_toBottomOf="parent"
             app:layout_constraintEnd_toEndOf="parent"
             app:layout_constraintStart_toStartOf="parent"
@@ -50,6 +51,7 @@
                 android:background="@drawable/rounded_rectangle"
                 android:contentDescription="@string/description_cover"
                 android:maxWidth="100dp"
+                android:scaleType="centerCrop"
                 tools:src="@mipmap/ic_launcher" />
 
             <LinearLayout

+ 1 - 0
app/src/main/res/layout/source_comfortable_grid_item.xml

@@ -25,6 +25,7 @@
                 android:layout_width="match_parent"
                 android:layout_height="match_parent"
                 android:background="?attr/colorSurface"
+                android:scaleType="centerCrop"
                 tools:ignore="ContentDescription"
                 tools:src="@mipmap/ic_launcher" />
 

+ 1 - 0
app/src/main/res/layout/source_compact_grid_item.xml

@@ -17,6 +17,7 @@
             android:layout_width="match_parent"
             android:layout_height="match_parent"
             android:background="?attr/colorSurface"
+            android:scaleType="centerCrop"
             tools:ignore="ContentDescription"
             tools:src="@mipmap/ic_launcher" />
 

+ 1 - 0
app/src/main/res/layout/source_list_item.xml

@@ -15,6 +15,7 @@
         android:layout_height="@dimen/material_component_lists_single_line_with_avatar_height"
         android:layout_gravity="center_vertical"
         android:padding="8dp"
+        android:scaleType="centerCrop"
         app:layout_constraintBottom_toBottomOf="parent"
         app:layout_constraintStart_toStartOf="parent"
         app:layout_constraintTop_toTopOf="parent"

+ 1 - 0
app/src/main/res/layout/updates_item.xml

@@ -14,6 +14,7 @@
         android:layout_marginStart="16dp"
         android:layout_marginTop="8dp"
         android:layout_marginBottom="8dp"
+        android:scaleType="centerCrop"
         app:layout_constraintBottom_toBottomOf="parent"
         app:layout_constraintDimensionRatio="h,1:1"
         app:layout_constraintStart_toStartOf="parent"