From 93e61367958e280689423b7692cd98cda59805e9 Mon Sep 17 00:00:00 2001 From: Ivan Iskandar <12537387+ivaniskandar@users.noreply.github.com> Date: Wed, 28 Apr 2021 19:32:00 +0700 Subject: [PATCH] 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 --- app/build.gradle.kts | 10 +- app/src/main/java/eu/kanade/tachiyomi/App.kt | 29 ++- .../kanade/tachiyomi/data/cache/CoverCache.kt | 8 + .../tachiyomi/data/coil/ByteBufferFetcher.kt | 25 +++ .../tachiyomi/data/coil/MangaCoverFetcher.kt | 172 ++++++++++++++++++ .../tachiyomi/data/glide/FileFetcher.kt | 60 ------ .../glide/LibraryMangaCustomCoverFetcher.kt | 25 --- .../data/glide/LibraryMangaUrlFetcher.kt | 86 --------- .../tachiyomi/data/glide/MangaThumbnail.kt | 15 -- .../data/glide/MangaThumbnailModelLoader.kt | 134 -------------- .../data/glide/PassthroughModelLoader.kt | 72 -------- .../tachiyomi/data/glide/TachiGlideModule.kt | 55 ------ .../data/library/LibraryUpdateNotifier.kt | 42 ++--- .../data/library/LibraryUpdateService.kt | 1 + .../kanade/tachiyomi/network/NetworkHelper.kt | 39 ++-- .../ui/browse/extension/ExtensionHolder.kt | 9 +- .../migration/manga/MigrationMangaHolder.kt | 24 +-- .../browse/SourceComfortableGridHolder.kt | 27 ++- .../browse/source/browse/SourceGridHolder.kt | 27 ++- .../browse/source/browse/SourceListHolder.kt | 29 ++- .../globalsearch/GlobalSearchCardHolder.kt | 28 +-- .../library/LibraryComfortableGridHolder.kt | 14 +- .../ui/library/LibraryCompactGridHolder.kt | 14 +- .../tachiyomi/ui/library/LibraryListHolder.kt | 24 +-- .../tachiyomi/ui/manga/MangaPresenter.kt | 2 + .../ui/manga/info/MangaInfoHeaderAdapter.kt | 19 +- .../ui/manga/track/TrackSearchAdapter.kt | 12 +- .../tachiyomi/ui/reader/ReaderPresenter.kt | 1 + .../tachiyomi/ui/reader/SaveImageNotifier.kt | 36 ++-- .../ui/reader/viewer/pager/PagerPageHolder.kt | 61 +++---- .../viewer/webtoon/WebtoonPageHolder.kt | 64 +++---- .../ui/recent/history/HistoryHolder.kt | 23 +-- .../ui/recent/updates/UpdatesHolder.kt | 24 +-- .../tachiyomi/widget/StateImageViewTarget.kt | 72 +++----- app/src/main/res/layout/manga_info_header.xml | 2 + .../layout/source_comfortable_grid_item.xml | 1 + .../res/layout/source_compact_grid_item.xml | 1 + app/src/main/res/layout/source_list_item.xml | 1 + app/src/main/res/layout/updates_item.xml | 1 + 39 files changed, 492 insertions(+), 797 deletions(-) create mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/coil/ByteBufferFetcher.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/coil/MangaCoverFetcher.kt delete mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/glide/FileFetcher.kt delete mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/glide/LibraryMangaCustomCoverFetcher.kt delete mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/glide/LibraryMangaUrlFetcher.kt delete mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/glide/MangaThumbnail.kt delete mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/glide/MangaThumbnailModelLoader.kt delete mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/glide/PassthroughModelLoader.kt delete mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/glide/TachiGlideModule.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index c86a2c3eb..dfce4f49d 100644 --- a/app/build.gradle.kts +++ b/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", ) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/App.kt b/app/src/main/java/eu/kanade/tachiyomi/App.kt index 6f236a0d7..c8fefff86 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/App.kt +++ b/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().coilClient) + crossfade(300) + allowRgb565(getSystemService()!!.isLowRamDevice) + }.build() + } + @OnLifecycleEvent(Lifecycle.Event.ON_STOP) @Suppress("unused") fun onAppBackgrounded() { diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/cache/CoverCache.kt b/app/src/main/java/eu/kanade/tachiyomi/data/cache/CoverCache.kt index 4d1a00607..fbdfc52bd 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/cache/CoverCache.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/cache/CoverCache.kt @@ -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() } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/coil/ByteBufferFetcher.kt b/app/src/main/java/eu/kanade/tachiyomi/data/coil/ByteBufferFetcher.kt new file mode 100644 index 000000000..78d9c7ecd --- /dev/null +++ b/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 { + override suspend fun fetch(pool: BitmapPool, data: ByteBuffer, size: Size, options: Options): FetchResult { + return SourceResult( + source = ByteArrayInputStream(data.array()).source().buffer(), + mimeType = null, + dataSource = DataSource.MEMORY + ) + } + + override fun key(data: ByteBuffer): String? = null +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/coil/MangaCoverFetcher.kt b/app/src/main/java/eu/kanade/tachiyomi/data/coil/MangaCoverFetcher.kt new file mode 100644 index 000000000..6aae106cd --- /dev/null +++ b/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 { + private val coverCache: CoverCache by injectLazy() + private val sourceManager: SourceManager by injectLazy() + private val defaultClient = Injekt.get().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 { + 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() + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/glide/FileFetcher.kt b/app/src/main/java/eu/kanade/tachiyomi/data/glide/FileFetcher.kt deleted file mode 100644 index a54c3cede..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/data/glide/FileFetcher.kt +++ /dev/null @@ -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 { - - private var data: InputStream? = null - - override fun loadData(priority: Priority, callback: DataFetcher.DataCallback) { - loadFromFile(callback) - } - - private fun loadFromFile(callback: DataFetcher.DataCallback) { - loadFromFile(File(filePath), callback) - } - - protected fun loadFromFile(file: File, callback: DataFetcher.DataCallback) { - 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 { - return InputStream::class.java - } - - override fun getDataSource(): DataSource { - return DataSource.LOCAL - } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/glide/LibraryMangaCustomCoverFetcher.kt b/app/src/main/java/eu/kanade/tachiyomi/data/glide/LibraryMangaCustomCoverFetcher.kt deleted file mode 100644 index 3d04f40c2..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/data/glide/LibraryMangaCustomCoverFetcher.kt +++ /dev/null @@ -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) { - getCustomCoverFile()?.let { - loadFromFile(it, callback) - } ?: callback.onLoadFailed(Exception("Custom cover file not found")) - } - - protected fun getCustomCoverFile(): File? { - return coverCache.getCustomCoverFile(manga).takeIf { it.exists() } - } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/glide/LibraryMangaUrlFetcher.kt b/app/src/main/java/eu/kanade/tachiyomi/data/glide/LibraryMangaUrlFetcher.kt deleted file mode 100644 index 5d40dd6a4..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/data/glide/LibraryMangaUrlFetcher.kt +++ /dev/null @@ -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, - private val manga: Manga, - private val coverCache: CoverCache -) : LibraryMangaCustomCoverFetcher(manga, coverCache) { - - override fun loadData(priority: Priority, callback: DataFetcher.DataCallback) { - 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 { - 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() - } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/glide/MangaThumbnail.kt b/app/src/main/java/eu/kanade/tachiyomi/data/glide/MangaThumbnail.kt deleted file mode 100644 index ae0057eb4..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/data/glide/MangaThumbnail.kt +++ /dev/null @@ -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) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/glide/MangaThumbnailModelLoader.kt b/app/src/main/java/eu/kanade/tachiyomi/data/glide/MangaThumbnailModelLoader.kt deleted file mode 100644 index b3cf79a15..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/data/glide/MangaThumbnailModelLoader.kt +++ /dev/null @@ -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 { - - /** - * 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().client - - /** - * Map where request headers are stored for a source. - */ - private val cachedHeaders = hashMapOf() - - /** - * Factory class for creating [MangaThumbnailModelLoader] instances. - */ - class Factory : ModelLoaderFactory { - - override fun build(multiFactory: MultiModelLoaderFactory): ModelLoader { - 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? { - 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() - } - } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/glide/PassthroughModelLoader.kt b/app/src/main/java/eu/kanade/tachiyomi/data/glide/PassthroughModelLoader.kt deleted file mode 100644 index dd6d546f8..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/data/glide/PassthroughModelLoader.kt +++ /dev/null @@ -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 { - - override fun buildLoadData( - model: InputStream, - width: Int, - height: Int, - options: Options - ): ModelLoader.LoadData? { - return ModelLoader.LoadData(ObjectKey(model), Fetcher(model)) - } - - override fun handles(model: InputStream): Boolean { - return true - } - - class Fetcher(private val stream: InputStream) : DataFetcher { - - override fun getDataClass(): Class { - 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 - ) { - callback.onDataReady(stream) - } - } - - /** - * Factory class for creating [PassthroughModelLoader] instances. - */ - class Factory : ModelLoaderFactory { - - override fun build( - multiFactory: MultiModelLoaderFactory - ): ModelLoader { - return PassthroughModelLoader() - } - - override fun teardown() {} - } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/glide/TachiGlideModule.kt b/app/src/main/java/eu/kanade/tachiyomi/data/glide/TachiGlideModule.kt deleted file mode 100644 index dca2e0879..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/data/glide/TachiGlideModule.kt +++ /dev/null @@ -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().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() - ) - } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateNotifier.kt b/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateNotifier.kt index ab292bc4e..d760fce94 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateNotifier.kt +++ b/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): Notification { + private suspend fun createNewChaptersNotification(manga: Manga, chapters: Array): 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): String { diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateService.kt b/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateService.kt index e6215e4c8..86a2b81a1 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateService.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateService.kt @@ -386,6 +386,7 @@ class LibraryUpdateService( } } + coverCache.clearMemoryCache() notifier.cancelProgressNotification() } diff --git a/app/src/main/java/eu/kanade/tachiyomi/network/NetworkHelper.kt b/app/src/main/java/eu/kanade/tachiyomi/network/NetworkHelper.kt index 4b729fdea..4d26a8d4c 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/network/NetworkHelper.kt +++ b/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()) + 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 + if (BuildConfig.DEBUG) { + val httpLoggingInterceptor = HttpLoggingInterceptor().apply { + level = HttpLoggingInterceptor.Level.HEADERS + } + builder.addInterceptor(httpLoggingInterceptor) } - builder.addInterceptor(httpLoggingInterceptor) + + when (preferences.dohProvider()) { + PREF_DOH_CLOUDFLARE -> builder.dohCloudflare() + PREF_DOH_GOOGLE -> builder.dohGoogle() + } + + return builder } - when (preferences.dohProvider()) { - PREF_DOH_CLOUDFLARE -> builder.dohCloudflare() - PREF_DOH_GOOGLE -> builder.dohGoogle() - } + val client by lazy { baseClientBuilder.cache(Cache(cacheDir, cacheSize)).build() } - builder.build() - } + val coilClient by lazy { baseClientBuilder.cache(CoilUtils.createDefaultCache(context)).build() } val cloudflareClient by lazy { client.newBuilder() diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/ExtensionHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/ExtensionHolder.kt index cdec1a95b..7f5a7158a 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/ExtensionHolder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/ExtensionHolder.kt @@ -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) } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/manga/MigrationMangaHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/manga/MigrationMangaHolder.kt index c979f755e..af63f683c 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/manga/MigrationMangaHolder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/manga/MigrationMangaHolder.kt @@ -1,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)) + } } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/SourceComfortableGridHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/SourceComfortableGridHolder.kt index 7b2236e32..62787a684 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/SourceComfortableGridHolder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/SourceComfortableGridHolder.kt @@ -1,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) } } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/SourceGridHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/SourceGridHolder.kt index d4ed34155..74ab0fd5e 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/SourceGridHolder.kt +++ b/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) } } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/SourceListHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/SourceListHolder.kt index 9b9af026b..5b98f5b48 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/SourceListHolder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/SourceListHolder.kt @@ -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) + } } } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/GlobalSearchCardHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/GlobalSearchCardHolder.kt index 8b0310f1f..d7c83b063 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/GlobalSearchCardHolder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/GlobalSearchCardHolder.kt @@ -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) } } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryComfortableGridHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryComfortableGridHolder.kt index 96aa02150..bb7684554 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryComfortableGridHolder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryComfortableGridHolder.kt @@ -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) } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryCompactGridHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryCompactGridHolder.kt index 851223a18..bcf0c254e 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryCompactGridHolder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryCompactGridHolder.kt @@ -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) } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryListHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryListHolder.kt index d7071f32b..3e67016ac 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryListHolder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryListHolder.kt @@ -2,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)) + } } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaPresenter.kt index 7dd1f653f..aafa7cd5f 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaPresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaPresenter.kt @@ -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()) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaInfoHeaderAdapter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaInfoHeaderAdapter.kt index 411d56d7c..dc5f4c37b 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaInfoHeaderAdapter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaInfoHeaderAdapter.kt @@ -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 diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackSearchAdapter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackSearchAdapter.kt index 2ee8ba352..af5c92b02 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackSearchAdapter.kt +++ b/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() diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderPresenter.kt index 933f01f5d..1cc098127 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderPresenter.kt +++ b/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 diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/SaveImageNotifier.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/SaveImageNotifier.kt index 2814b412d..98e844304 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/SaveImageNotifier.kt +++ b/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) { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerPageHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerPageHolder.kt index 2a9cf6635..f5abc2728 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerPageHolder.kt +++ b/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 { - override fun onLoadFailed( - e: GlideException?, - model: Any?, - target: Target?, - isFirstResource: Boolean - ): Boolean { - onImageDecodeError() - return false - } - - override fun onResourceReady( - resource: Drawable?, - model: Any?, - target: Target?, - 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) } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonPageHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonPageHolder.kt index fc72f0974..5092572b2 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonPageHolder.kt +++ b/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 { - override fun onLoadFailed( - e: GlideException?, - model: Any?, - target: Target?, - isFirstResource: Boolean - ): Boolean { - onImageDecodeError() - return false - } - - override fun onResourceReady( - resource: Drawable?, - model: Any?, - target: Target?, - 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) } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/recent/history/HistoryHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/recent/history/HistoryHolder.kt index 3d10d931d..8661cb845 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/recent/history/HistoryHolder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/recent/history/HistoryHolder.kt @@ -1,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)) + } } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/recent/updates/UpdatesHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/recent/updates/UpdatesHolder.kt index e0afdcbdc..e3226de05 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/recent/updates/UpdatesHolder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/recent/updates/UpdatesHolder.kt @@ -2,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)) + } } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/widget/StateImageViewTarget.kt b/app/src/main/java/eu/kanade/tachiyomi/widget/StateImageViewTarget.kt index a48800230..0600e5d61 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/widget/StateImageViewTarget.kt +++ b/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(view) { - - private var resource: Drawable? = null - - private val imageScaleType = view.scaleType - - override fun setResource(resource: Drawable?) { - view.setImageDrawable(resource) + 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 onLoadStarted(placeholder: Drawable?) { - progress?.isVisible = true - super.onLoadStarted(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 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) - } - - override fun onLoadCleared(placeholder: Drawable?) { - progress?.isVisible = false - super.onLoadCleared(placeholder) - } - - override fun onResourceReady(resource: Drawable, transition: Transition?) { - 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) + } } } diff --git a/app/src/main/res/layout/manga_info_header.xml b/app/src/main/res/layout/manga_info_header.xml index 7e44508e7..3f2eaa407 100644 --- a/app/src/main/res/layout/manga_info_header.xml +++ b/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" /> diff --git a/app/src/main/res/layout/source_compact_grid_item.xml b/app/src/main/res/layout/source_compact_grid_item.xml index 0641f9fe0..7ccc0ccdb 100644 --- a/app/src/main/res/layout/source_compact_grid_item.xml +++ b/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" /> diff --git a/app/src/main/res/layout/source_list_item.xml b/app/src/main/res/layout/source_list_item.xml index 7fb8528b7..63c1272db 100644 --- a/app/src/main/res/layout/source_list_item.xml +++ b/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" diff --git a/app/src/main/res/layout/updates_item.xml b/app/src/main/res/layout/updates_item.xml index 600597b30..7abe05932 100644 --- a/app/src/main/res/layout/updates_item.xml +++ b/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"