diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 4eb87837ac..4d02fe35d2 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -76,8 +76,8 @@ android { } compileOptions { - setSourceCompatibility(1.8) - setTargetCompatibility(1.8) + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 } kotlinOptions { jvmTarget = "1.8" @@ -141,6 +141,21 @@ dependencies { debugImplementation ("com.github.ChuckerTeam.Chucker:library:$chuckerVersion") releaseImplementation ("com.github.ChuckerTeam.Chucker:library-no-op:$chuckerVersion") + //hyperion + val hyperionVersion = "0.9.27" + debugImplementation("com.willowtreeapps.hyperion:hyperion-core:$hyperionVersion") + debugImplementation("com.willowtreeapps.hyperion:hyperion-timber:$hyperionVersion") + debugImplementation("com.willowtreeapps.hyperion:hyperion-core:$hyperionVersion") + debugImplementation("com.willowtreeapps.hyperion:hyperion-attr:$hyperionVersion") + debugImplementation("com.willowtreeapps.hyperion:hyperion-build-config:$hyperionVersion") + debugImplementation("com.willowtreeapps.hyperion:hyperion-crash:$hyperionVersion") + debugImplementation("com.willowtreeapps.hyperion:hyperion-disk:$hyperionVersion") + debugImplementation("com.willowtreeapps.hyperion:hyperion-geiger-counter:$hyperionVersion") + debugImplementation("com.willowtreeapps.hyperion:hyperion-measurement:$hyperionVersion") + debugImplementation("com.willowtreeapps.hyperion:hyperion-phoenix:$hyperionVersion") + debugImplementation("com.willowtreeapps.hyperion:hyperion-recorder:$hyperionVersion") + debugImplementation("com.willowtreeapps.hyperion:hyperion-shared-preferences:$hyperionVersion") + // REST val retrofitVersion = "2.7.2" implementation("com.squareup.retrofit2:retrofit:$retrofitVersion") @@ -184,10 +199,10 @@ dependencies { implementation("com.github.inorichi.injekt:injekt-core:65b0440") // Image library - val glideVersion = "4.11.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 = "0.10.1" + implementation("io.coil-kt:coil:$coilVersion") + implementation("io.coil-kt:coil-gif:$coilVersion") + implementation("io.coil-kt:coil-svg:$coilVersion") // Logging implementation("com.jakewharton.timber:timber:4.7.1") diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index 0d0bde354f..089369db4e 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -27,14 +27,6 @@ -dontwarn javax.annotation.** -dontwarn retrofit2.Platform$Java8 -# Glide specific rules # -# https://github.com/bumptech/glide --keep public class * implements com.bumptech.glide.module.GlideModule --keep public class * extends com.bumptech.glide.AppGlideModule --keep public enum com.bumptech.glide.load.resource.bitmap.ImageHeaderParser$** { - **[] $VALUES; - public *; -} # RxJava 1.1.0 -dontwarn sun.misc.** diff --git a/app/src/main/java/eu/kanade/tachiyomi/App.kt b/app/src/main/java/eu/kanade/tachiyomi/App.kt index 69434a0f50..5e16ff7807 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/App.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/App.kt @@ -8,6 +8,7 @@ import androidx.lifecycle.LifecycleObserver import androidx.lifecycle.OnLifecycleEvent import androidx.lifecycle.ProcessLifecycleOwner import androidx.multidex.MultiDex +import eu.kanade.tachiyomi.data.download.coil.CoilSetup import eu.kanade.tachiyomi.data.notification.Notifications import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.getOrDefault @@ -18,6 +19,7 @@ import org.acra.annotation.ReportsCrashes import timber.log.Timber import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.InjektScope +import uy.kohesive.injekt.api.get import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.registry.default.DefaultRegistrar @@ -37,6 +39,7 @@ open class App : Application(), LifecycleObserver { Injekt = InjektScope(DefaultRegistrar()) Injekt.importModule(AppModule(this)) + CoilSetup(this) setupAcra() setupNotificationChannels() 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 0fb232cc94..88d620165e 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 @@ -2,8 +2,10 @@ package eu.kanade.tachiyomi.data.cache import android.content.Context import android.text.format.Formatter +import coil.Coil import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.database.DatabaseHelper +import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.util.storage.DiskUtil import eu.kanade.tachiyomi.util.system.executeOnIO import eu.kanade.tachiyomi.util.system.toast @@ -11,6 +13,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import okhttp3.Cache import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get import java.io.File @@ -20,28 +23,31 @@ import java.io.InputStream /** * Class used to create cover cache. * It is used to store the covers of the library. - * Makes use of Glide (which can avoid repeating requests) to download covers. * Names of files are created with the md5 of the thumbnail URL. * * @param context the application context. * @constructor creates an instance of the cover cache. */ -class CoverCache(private val context: Context) { +class CoverCache(val context: Context) { /** * Cache directory used for cache management. */ private val cacheDir = context.getExternalFilesDir("covers") - ?: File(context.filesDir, "covers").also { it.mkdirs() } + ?: File(context.filesDir, "covers").also { it.mkdirs() } + + val cache = Cache(cacheDir, 300 * 1024 * 1024) // 300MB + + fun getChapterCacheSize(): String { + return Formatter.formatFileSize(context, DiskUtil.getDirectorySize(cacheDir)) + } fun deleteOldCovers() { GlobalScope.launch(Dispatchers.Default) { val db = Injekt.get() var deletedSize = 0L val urls = db.getLibraryMangas().executeOnIO().mapNotNull { - it.thumbnail_url?.let { url -> - return@mapNotNull DiskUtil.hashKeyForDisk(url) - } + it.thumbnail_url?.let { url -> return@mapNotNull it.key() } null } val files = cacheDir.listFiles()?.iterator() ?: return@launch @@ -68,8 +74,8 @@ class CoverCache(private val context: Context) { * @param thumbnailUrl the thumbnail url. * @return cover image. */ - fun getCoverFile(thumbnailUrl: String): File { - return File(cacheDir, DiskUtil.hashKeyForDisk(thumbnailUrl)) + fun getCoverFile(manga: Manga): File { + return File(cacheDir, manga.key()) } /** @@ -80,26 +86,26 @@ class CoverCache(private val context: Context) { * @throws IOException if there's any error. */ @Throws(IOException::class) - fun copyToCache(thumbnailUrl: String, inputStream: InputStream) { + fun copyToCache(manga: Manga, inputStream: InputStream) { // Get destination file. - val destFile = getCoverFile(thumbnailUrl) + val destFile = getCoverFile(manga) destFile.outputStream().use { inputStream.copyTo(it) } } /** - * Delete the cover file from the cache. + * Delete the cover file from the disk cache and optional from memory cache * * @param thumbnailUrl the thumbnail url. * @return status of deletion. */ - fun deleteFromCache(thumbnailUrl: String?): Boolean { + fun deleteFromCache(manga: Manga, deleteMemoryCache: Boolean = true) { // Check if url is empty. - if (thumbnailUrl.isNullOrEmpty()) - return false + if (manga.thumbnail_url.isNullOrEmpty()) return - // Remove file. - val file = getCoverFile(thumbnailUrl) - return file.exists() && file.delete() + // Remove file + val file = getCoverFile(manga) + if (deleteMemoryCache) Coil.imageLoader(context).invalidate(file.name) + if (file.exists()) file.delete() } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/models/Manga.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/models/Manga.kt index 8881c3a46e..e38d8157fc 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/database/models/Manga.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/models/Manga.kt @@ -5,9 +5,11 @@ import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.source.SourceManager import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.ui.reader.ReaderActivity +import eu.kanade.tachiyomi.util.storage.DiskUtil import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get import java.util.Locale +import kotlin.random.Random interface Manga : SManga { @@ -52,13 +54,15 @@ interface Manga : SManga { fun showChapterTitle(defaultShow: Boolean): Boolean = chapter_flags and DISPLAY_MASK == DISPLAY_NUMBER fun mangaType(context: Context): String { - return context.getString(when (mangaType()) { - TYPE_WEBTOON -> R.string.webtoon - TYPE_MANHWA -> R.string.manhwa - TYPE_MANHUA -> R.string.manhua - TYPE_COMIC -> R.string.comic - else -> R.string.manga - }).toLowerCase(Locale.getDefault()) + return context.getString( + when (mangaType()) { + TYPE_WEBTOON -> R.string.webtoon + TYPE_MANHWA -> R.string.manhwa + TYPE_MANHUA -> R.string.manhua + TYPE_COMIC -> R.string.comic + else -> R.string.manga + } + ).toLowerCase(Locale.getDefault()) } /** @@ -97,14 +101,16 @@ interface Manga : SManga { return if (currentTags?.any { tag -> tag == "long strip" || tag == "manhwa" || tag.contains("webtoon") - } == true || isWebtoonSource(sourceName)) + } == true || isWebtoonSource(sourceName) + ) ReaderActivity.WEBTOON else if (currentTags?.any { tag -> tag == "chinese" || tag == "manhua" || tag.startsWith("english") || tag == "comic" } == true || (isComicSource(sourceName) && !sourceName.contains("tapastic", true)) || - sourceName.contains("manhua", true)) + sourceName.contains("manhua", true) + ) ReaderActivity.LEFT_TO_RIGHT else 0 } @@ -139,6 +145,19 @@ interface Manga : SManga { sourceName.contains("tapastic", true) } + fun key(): String { + return DiskUtil.hashKeyForDisk(thumbnail_url.orEmpty()) + } + + fun setCustomThumbnailUrl() { + removeCustomThumbnailUrl() + thumbnail_url = "Custom-${Random.nextInt(0, 1000)}-J2K-${thumbnail_url ?: id!!}" + } + + fun removeCustomThumbnailUrl() { + thumbnail_url = thumbnail_url?.substringAfter("-J2K-")?.substringAfter("Custom-") + } + // Used to display the chapter's title one way or another var displayMode: Int get() = chapter_flags and DISPLAY_MASK diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/models/MangaImpl.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/models/MangaImpl.kt index 2c229a6486..33fc80a2fd 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/database/models/MangaImpl.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/models/MangaImpl.kt @@ -67,14 +67,4 @@ open class MangaImpl : Manga { if (::url.isInitialized) return url.hashCode() else return (id ?: 0L).hashCode() } - - companion object { - private var lastCoverFetch: HashMap = hashMapOf() - - fun setLastCoverFetch(id: Long, time: Long) { - lastCoverFetch[id] = time - } - - fun getLastCoverFetch(id: Long) = lastCoverFetch[id] ?: 0 - } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/download/coil/ByteArrayFetcher.kt b/app/src/main/java/eu/kanade/tachiyomi/data/download/coil/ByteArrayFetcher.kt new file mode 100644 index 0000000000..84bab7f640 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/download/coil/ByteArrayFetcher.kt @@ -0,0 +1,31 @@ +package eu.kanade.tachiyomi.data.download.coil + +import coil.bitmappool.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 + +class ByteArrayFetcher : Fetcher { + + override fun key(data: ByteArray): String? = null + + override suspend fun fetch( + pool: BitmapPool, + data: ByteArray, + size: Size, + options: Options + ): FetchResult { + val source = ByteArrayInputStream(data).source().buffer() + return SourceResult( + source = ByteArrayInputStream(data).source().buffer(), + mimeType = "image/gif", + dataSource = DataSource.MEMORY + ) + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/download/coil/CoilSetup.kt b/app/src/main/java/eu/kanade/tachiyomi/data/download/coil/CoilSetup.kt new file mode 100644 index 0000000000..6ccb44b1e3 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/download/coil/CoilSetup.kt @@ -0,0 +1,42 @@ +package eu.kanade.tachiyomi.data.download.coil + +import android.content.Context +import android.os.Build +import coil.Coil +import coil.ImageLoader +import coil.decode.GifDecoder +import coil.decode.ImageDecoderDecoder +import coil.decode.SvgDecoder +import coil.util.CoilUtils +import com.chuckerteam.chucker.api.ChuckerInterceptor +import eu.kanade.tachiyomi.R +import okhttp3.OkHttpClient + +class CoilSetup(context: Context) { + init { + val imageLoader = ImageLoader.Builder(context) + .availableMemoryPercentage(0.40) + .crossfade(true) + .allowRgb565(true) + .allowHardware(false) + .error(R.drawable.ic_broken_image_grey_24dp) + .componentRegistry { + if (Build.VERSION.SDK_INT >= 28) { + add(ImageDecoderDecoder()) + } else { + add(GifDecoder()) + } + add(SvgDecoder(context)) + add(MangaFetcher()) + add(ByteArrayFetcher()) + }.okHttpClient { + OkHttpClient.Builder() + .cache(CoilUtils.createDefaultCache(context)) + .addInterceptor(ChuckerInterceptor(context)) + .build() + } + .build() + + Coil.setImageLoader(imageLoader) + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/download/coil/MangaFetcher.kt b/app/src/main/java/eu/kanade/tachiyomi/data/download/coil/MangaFetcher.kt new file mode 100644 index 0000000000..ac0f86cda5 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/download/coil/MangaFetcher.kt @@ -0,0 +1,125 @@ +package eu.kanade.tachiyomi.data.download.coil + +import coil.bitmappool.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 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.storage.DiskUtil +import okhttp3.Call +import okhttp3.Request +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 + +class MangaFetcher() : Fetcher { + + private val coverCache: CoverCache by injectLazy() + private val sourceManager: SourceManager by injectLazy() + private val defaultClient = Injekt.get().client + + override fun key(manga: Manga): String? { + if (manga.thumbnail_url.isNullOrBlank()) return null + return DiskUtil.hashKeyForDisk(manga.thumbnail_url!!) + } + + override suspend fun fetch(pool: BitmapPool, manga: Manga, size: Size, options: Options): FetchResult { + val cover = manga.thumbnail_url + when (getResourceType(cover)) { + Type.File -> { + return fileLoader(manga) + } + Type.URL -> { + return httpLoader(manga) + } + Type.CUSTOM -> { + return customLoader(manga) + } + null -> error("Invalid image") + } + } + + private fun customLoader(manga: Manga): FetchResult { + val coverFile = coverCache.getCoverFile(manga) + if (coverFile.exists()) { + return fileLoader(coverFile) + } + manga.thumbnail_url = manga.thumbnail_url!!.substringAfter("-J2K-").substringAfter("CUSTOM-") + return httpLoader(manga) + } + + private fun httpLoader(manga: Manga): FetchResult { + val coverFile = coverCache.getCoverFile(manga) + if (coverFile.exists()) { + return fileLoader(coverFile) + } + val call = getCall(manga) + val tmpFile = File(coverFile.absolutePath + "_tmp") + + val response = call.execute() + val body = checkNotNull(response.body) { "Null response source" } + + body.source().use { input -> + tmpFile.sink().buffer().use { output -> + output.writeAll(input) + } + } + + tmpFile.renameTo(coverFile) + return fileLoader(coverFile) + } + + 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 getCall(manga: Manga): Call { + val source = sourceManager.get(manga.source) as? HttpSource + val client = source?.client ?: defaultClient + + val newClient = client.newBuilder() + .cache(coverCache.cache) + .build() + + val request = Request.Builder().url(manga.thumbnail_url!!).also { + if (source != null) { + it.headers(source.headers) + } + }.build() + + return newClient.newCall(request) + } + + private fun getResourceType(cover: String?): Type? { + return when { + cover.isNullOrEmpty() -> null + cover.startsWith("http") -> Type.URL + cover.startsWith("Custom-") -> Type.CUSTOM + cover.startsWith("/") || cover.startsWith("file://") -> Type.File + else -> null + } + } + + private enum class Type { + File, CUSTOM, URL; + } +} 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 6f88ec722d..0000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/data/glide/FileFetcher.kt +++ /dev/null @@ -1,55 +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 java.io.File -import java.io.FileInputStream -import java.io.FileNotFoundException -import java.io.IOException -import java.io.InputStream - -open class FileFetcher(private val file: File) : DataFetcher { - - private var data: InputStream? = null - - override fun loadData(priority: Priority, callback: DataFetcher.DataCallback) { - loadFromFile(callback) - } - - protected fun loadFromFile(callback: DataFetcher.DataCallback) { - try { - data = FileInputStream(file) - } catch (e: FileNotFoundException) { - if (Log.isLoggable(TAG, Log.DEBUG)) { - Log.d(TAG, "Failed to open file", e) - } - 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/LibraryMangaUrlFetcher.kt b/app/src/main/java/eu/kanade/tachiyomi/data/glide/LibraryMangaUrlFetcher.kt deleted file mode 100644 index f27f6e5363..0000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/data/glide/LibraryMangaUrlFetcher.kt +++ /dev/null @@ -1,72 +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.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 file: File -) : -FileFetcher(file) { - - override fun loadData(priority: Priority, callback: DataFetcher.DataCallback) { - if (!file.exists()) { - networkFetcher.loadData(priority, object : DataFetcher.DataCallback { - override fun onDataReady(data: InputStream?) { - if (data != null) { - val tmpFile = File(file.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(file) - loadFromFile(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(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/MangaModelLoader.kt b/app/src/main/java/eu/kanade/tachiyomi/data/glide/MangaModelLoader.kt deleted file mode 100644 index 7a9c99f45d..0000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/data/glide/MangaModelLoader.kt +++ /dev/null @@ -1,156 +0,0 @@ -package eu.kanade.tachiyomi.data.glide - -import android.util.LruCache -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 uy.kohesive.injekt.Injekt -import uy.kohesive.injekt.api.get -import uy.kohesive.injekt.injectLazy -import java.io.File -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 MangaModelLoader : 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 - - /** - * LRU cache whose key is the thumbnail url of the manga, and the value contains the request url - * and the file where it should be stored in case the manga is a favorite. - */ - private val lruCache = LruCache(100) - - /** - * Map where request headers are stored for a source. - */ - private val cachedHeaders = hashMapOf() - - /** - * Factory class for creating [MangaModelLoader] instances. - */ - class Factory : ModelLoaderFactory { - - override fun build(multiFactory: MultiModelLoaderFactory): ModelLoader { - return MangaModelLoader() - } - - override fun teardown() {} - } - - override fun handles(model: Manga): Boolean { - return true - } - - /** - * Returns a fetcher for the given manga or null if the url is empty. - * - * @param manga 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( - manga: Manga, - width: Int, - height: Int, - options: Options - ): ModelLoader.LoadData? { - // Check thumbnail is not null or empty - val url = manga.thumbnail_url - - 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) - } - - // Obtain the file for this url from the LRU cache, or retrieve and add it to the cache. - val file = lruCache.getOrPut(glideUrl) { coverCache.getCoverFile(url) } - - val libraryFetcher = LibraryMangaUrlFetcher(networkFetcher, manga, file) - - // Return an instance of the fetcher providing the needed elements. - return ModelLoader.LoadData(MangaSignature(manga, file), libraryFetcher) - } else { - // Get the file from the url, removing the scheme if present, or from the cache if no url. - val file = when { - manga.hasCustomCover() -> coverCache.getCoverFile(manga.thumbnail_url!!) - url != null -> File(url.substringAfter("file://")) - else -> null - } - - if (file?.exists() != true) return null - - // Return an instance of the fetcher providing the needed elements. - return ModelLoader.LoadData(MangaSignature(manga, file), FileFetcher(file)) - } - } - - /** - * Returns the request headers for a source copying its OkHttp headers and caching them. - * - * @param manga the model. - */ - 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() - } - } - - private inline fun LruCache.getOrPut(key: K, defaultValue: () -> V): V { - val value = get(key) - return if (value == null) { - val answer = defaultValue() - put(key, answer) - answer - } else { - value - } - } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/glide/MangaSignature.kt b/app/src/main/java/eu/kanade/tachiyomi/data/glide/MangaSignature.kt deleted file mode 100644 index cdf880e426..0000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/data/glide/MangaSignature.kt +++ /dev/null @@ -1,27 +0,0 @@ -package eu.kanade.tachiyomi.data.glide - -import com.bumptech.glide.load.Key -import eu.kanade.tachiyomi.data.database.models.Manga -import java.io.File -import java.security.MessageDigest - -class MangaSignature(manga: Manga, file: File) : Key { - - private val key = manga.thumbnail_url + file.lastModified() - - override fun equals(other: Any?): Boolean { - return if (other is MangaSignature) { - key == other.key - } else { - false - } - } - - override fun hashCode(): Int { - return key.hashCode() - } - - override fun updateDiskCacheKey(md: MessageDigest) { - md.update(key.toByteArray(Key.CHARSET)) - } -} 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 dd6d546f8c..0000000000 --- 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 fffdbc433e..0000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/data/glide/TachiGlideModule.kt +++ /dev/null @@ -1,39 +0,0 @@ -package eu.kanade.tachiyomi.data.glide - -import android.content.Context -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.module.AppGlideModule -import com.bumptech.glide.request.RequestOptions -import eu.kanade.tachiyomi.data.database.models.Manga -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)) - } - - 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(Manga::class.java, InputStream::class.java, MangaModelLoader.Factory()) - registry.append(InputStream::class.java, InputStream::class.java, PassthroughModelLoader - .Factory()) - } -} 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 b35342e4b8..bbd883dad3 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 @@ -6,6 +6,7 @@ import android.app.Service import android.content.Context import android.content.Intent import android.graphics.BitmapFactory +import android.graphics.drawable.BitmapDrawable import android.os.Build import android.os.IBinder import android.os.PowerManager @@ -13,16 +14,18 @@ import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat.GROUP_ALERT_SUMMARY import androidx.core.app.NotificationManagerCompat import androidx.core.content.ContextCompat +import coil.Coil +import coil.request.CachePolicy +import coil.request.LoadRequest +import coil.transform.CircleCropTransformation import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.database.DatabaseHelper import eu.kanade.tachiyomi.data.database.models.Category import eu.kanade.tachiyomi.data.database.models.Chapter import eu.kanade.tachiyomi.data.database.models.LibraryManga import eu.kanade.tachiyomi.data.database.models.Manga -import eu.kanade.tachiyomi.data.database.models.MangaImpl import eu.kanade.tachiyomi.data.download.DownloadManager import eu.kanade.tachiyomi.data.download.DownloadService -import eu.kanade.tachiyomi.data.glide.GlideApp import eu.kanade.tachiyomi.data.library.LibraryUpdateRanker.rankingScheme import eu.kanade.tachiyomi.data.library.LibraryUpdateService.Companion.start import eu.kanade.tachiyomi.data.notification.NotificationReceiver @@ -56,7 +59,6 @@ import timber.log.Timber import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get import java.util.ArrayList -import java.util.Date import java.util.concurrent.TimeUnit import java.util.concurrent.atomic.AtomicInteger @@ -530,10 +532,16 @@ class LibraryUpdateService( val thumbnailUrl = manga.thumbnail_url manga.copyFrom(networkManga) manga.initialized = true - db.insertManga(manga).executeAsBlocking() - if (thumbnailUrl != networkManga.thumbnail_url && !manga.hasCustomCover()) { - MangaImpl.setLastCoverFetch(manga.id!!, Date().time) + // load new covers in background + if (!manga.hasCustomCover()) { + val request = LoadRequest.Builder(this@LibraryUpdateService) + .data(manga) + .memoryCachePolicy(CachePolicy.DISABLED) + .build() + Coil.imageLoader(this@LibraryUpdateService).execute(request) } + + db.insertManga(manga).executeAsBlocking() } } } @@ -604,10 +612,12 @@ class LibraryUpdateService( notifications.add(Pair(notification(Notifications.CHANNEL_NEW_CHAPTERS) { setSmallIcon(R.drawable.ic_tachi) try { - val icon = GlideApp.with(this@LibraryUpdateService) - .asBitmap().load(manga).dontTransform().centerCrop().circleCrop() - .override(256, 256).submit().get() - setLargeIcon(icon) + + val request = LoadRequest.Builder(this@LibraryUpdateService).data(manga) + .transformations(CircleCropTransformation()).size(width = 256, height = 256) + .target { drawable -> setLargeIcon((drawable as BitmapDrawable).bitmap) }.build() + + Coil.imageLoader(this@LibraryUpdateService).execute(request) } catch (e: Exception) { } setGroupAlertBehavior(GROUP_ALERT_SUMMARY) diff --git a/app/src/main/java/eu/kanade/tachiyomi/source/LocalSource.kt b/app/src/main/java/eu/kanade/tachiyomi/source/LocalSource.kt index 39b35682ab..a1df72194f 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/source/LocalSource.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/source/LocalSource.kt @@ -47,8 +47,8 @@ class LocalSource(private val context: Context) : CatalogueSource { return null } val cover = File("${dir.absolutePath}/${manga.url}", COVER_NAME) + if (cover.exists()) cover.delete() - // It might not exist if using the external SD card cover.parentFile?.mkdirs() input.use { cover.outputStream().use { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/extension/ExtensionHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/extension/ExtensionHolder.kt index 515f6a7590..701af23e1d 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/extension/ExtensionHolder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/extension/ExtensionHolder.kt @@ -4,15 +4,18 @@ import android.content.res.ColorStateList import android.graphics.Color import android.view.View import androidx.core.content.ContextCompat +import coil.api.clear +import coil.api.load import eu.kanade.tachiyomi.R -import eu.kanade.tachiyomi.data.glide.GlideApp import eu.kanade.tachiyomi.extension.model.Extension import eu.kanade.tachiyomi.extension.model.InstallStep import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder import eu.kanade.tachiyomi.util.system.LocaleHelper import eu.kanade.tachiyomi.util.system.getResourceColor import eu.kanade.tachiyomi.util.view.resetStrokeColor +import eu.kanade.tachiyomi.widget.CoverViewTarget import kotlinx.android.synthetic.main.extension_card_item.* +import kotlinx.android.synthetic.main.source_global_search_controller_card_item.* class ExtensionHolder(view: View, val adapter: ExtensionAdapter) : BaseFlexibleViewHolder(view, adapter) { @@ -35,11 +38,12 @@ class ExtensionHolder(view: View, val adapter: ExtensionAdapter) : itemView.context.getString(R.string.untrusted).toUpperCase() } - GlideApp.with(itemView.context).clear(edit_button) + edit_button.clear() + if (extension is Extension.Available) { - GlideApp.with(itemView.context) - .load(extension.iconUrl) - .into(edit_button) + edit_button.load(extension.iconUrl) { + target(CoverViewTarget(edit_button, progress)) + } } else { extension.getApplicationIcon(itemView.context)?.let { edit_button.setImageDrawable(it) } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryGridHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryGridHolder.kt index 1f7a662ed6..be11cb6ad9 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryGridHolder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryGridHolder.kt @@ -4,11 +4,11 @@ import android.app.Activity import android.view.Gravity import android.view.View import android.widget.FrameLayout -import com.bumptech.glide.load.engine.DiskCacheStrategy -import com.bumptech.glide.signature.ObjectKey +import coil.api.clear +import coil.api.loadAny +import coil.size.Precision +import coil.size.Scale import eu.kanade.tachiyomi.data.database.models.Manga -import eu.kanade.tachiyomi.data.database.models.MangaImpl -import eu.kanade.tachiyomi.data.glide.GlideApp import eu.kanade.tachiyomi.util.view.gone import eu.kanade.tachiyomi.util.view.visibleIf import kotlinx.android.synthetic.main.manga_grid_item.* @@ -65,29 +65,26 @@ class LibraryGridHolder( setReadingButton(item) // Update the cover. - if (item.manga.thumbnail_url == null) GlideApp.with(view.context).clear(cover_thumbnail) + if (item.manga.thumbnail_url == null) cover_thumbnail.clear() else { - val id = item.manga.id ?: return if (cover_thumbnail.height == 0) { val oldPos = adapterPosition adapter.recyclerView.post { if (oldPos == adapterPosition) - setCover(item.manga, id) + setCover(item.manga) } - } else setCover(item.manga, id) + } else setCover(item.manga) } } - private fun setCover(manga: Manga, id: Long) { + private fun setCover(manga: Manga) { if ((adapter.recyclerView.context as? Activity)?.isDestroyed == true) return - GlideApp.with(adapter.recyclerView.context).load(manga) - .diskCacheStrategy(DiskCacheStrategy.RESOURCE) - .signature(ObjectKey(MangaImpl.getLastCoverFetch(id).toString())) - .apply { - if (fixedSize) centerCrop() - else override(cover_thumbnail.maxHeight) + cover_thumbnail.loadAny(manga) { + if (!fixedSize) { + precision(Precision.INEXACT) + scale(Scale.FIT) } - .into(cover_thumbnail) + } } private fun playButtonClicked() { 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 aeca0f28b9..748e79144c 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,19 +2,19 @@ package eu.kanade.tachiyomi.ui.library import android.view.View import android.view.ViewGroup -import com.bumptech.glide.Glide -import com.bumptech.glide.load.engine.DiskCacheStrategy -import com.bumptech.glide.signature.ObjectKey +import coil.api.clear +import coil.api.loadAny +import coil.transform.RoundedCornersTransformation import eu.kanade.tachiyomi.R -import eu.kanade.tachiyomi.data.database.models.MangaImpl -import eu.kanade.tachiyomi.data.glide.GlideApp import eu.kanade.tachiyomi.util.system.dpToPx import eu.kanade.tachiyomi.util.view.gone import eu.kanade.tachiyomi.util.view.updateLayoutParams import eu.kanade.tachiyomi.util.view.visible import eu.kanade.tachiyomi.util.view.visibleIf import kotlinx.android.synthetic.main.manga_list_item.* +import kotlinx.android.synthetic.main.manga_list_item.title import kotlinx.android.synthetic.main.manga_list_item.view.* +import kotlinx.android.synthetic.main.recently_read_item.* import kotlinx.android.synthetic.main.unread_download_badge.* /** @@ -78,15 +78,13 @@ class LibraryListHolder( } // Update the cover. - if (item.manga.thumbnail_url == null) Glide.with(view.context).clear(cover_thumbnail) - else { + if (item.manga.thumbnail_url == null) { + cover_thumbnail.clear() + } else { val id = item.manga.id ?: return - - GlideApp.with(view.context).load(item.manga) - .diskCacheStrategy(DiskCacheStrategy.RESOURCE) - .signature(ObjectKey(MangaImpl.getLastCoverFetch(id).toString())) - .centerCrop() - .into(cover_thumbnail) + cover_thumbnail.loadAny(item.manga) { + transformations(RoundedCornersTransformation(2f, 2f, 2f, 2f)) + } } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryPresenter.kt index 9269204fe4..353079201a 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryPresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryPresenter.kt @@ -641,7 +641,7 @@ class LibraryPresenter( val mangaToDelete = mangas.distinctBy { it.id } mangaToDelete.forEach { manga -> db.resetMangaInfo(manga).executeOnIO() - coverCache.deleteFromCache(manga.thumbnail_url) + coverCache.deleteFromCache(manga) val source = sourceManager.get(manga.source) as? HttpSource if (source != null) downloadManager.deleteManga(manga, source) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/EditMangaDialog.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/EditMangaDialog.kt index 22fba0e922..e81336ebc1 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/EditMangaDialog.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/EditMangaDialog.kt @@ -4,15 +4,12 @@ import android.app.Dialog import android.net.Uri import android.os.Bundle import android.view.View +import coil.api.loadAny import com.afollestad.materialdialogs.MaterialDialog import com.afollestad.materialdialogs.customview.customView -import com.bumptech.glide.load.engine.DiskCacheStrategy -import com.bumptech.glide.signature.ObjectKey import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.database.DatabaseHelper import eu.kanade.tachiyomi.data.database.models.Manga -import eu.kanade.tachiyomi.data.database.models.MangaImpl -import eu.kanade.tachiyomi.data.glide.GlideApp import eu.kanade.tachiyomi.source.LocalSource import eu.kanade.tachiyomi.ui.base.controller.DialogController import kotlinx.android.synthetic.main.edit_manga_dialog.view.* @@ -60,13 +57,7 @@ class EditMangaDialog : DialogController { } fun onViewCreated(view: View) { - GlideApp.with(view.context) - .asDrawable() - .load(manga) - .diskCacheStrategy(DiskCacheStrategy.RESOURCE) - .signature(ObjectKey(MangaImpl.getLastCoverFetch(manga.id!!).toString())) - .dontAnimate() - .into(view.manga_cover) + view.manga_cover.loadAny(manga) val isLocal = manga.source == LocalSource.ID if (isLocal) { @@ -93,7 +84,7 @@ class EditMangaDialog : DialogController { } fun updateCover(uri: Uri) { - GlideApp.with(dialogView!!.context).load(uri).into(dialogView!!.manga_cover) + dialogView!!.manga_cover.loadAny(uri) customCoverUri = uri } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaDetailsController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaDetailsController.kt index cf708b8f3e..af7c81cccf 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaDetailsController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaDetailsController.kt @@ -11,11 +11,9 @@ import android.content.ClipData import android.content.ClipboardManager import android.content.Context import android.content.Intent -import android.graphics.Bitmap import android.graphics.Color import android.graphics.Rect import android.graphics.drawable.BitmapDrawable -import android.graphics.drawable.Drawable import android.os.Build import android.os.Bundle import android.view.LayoutInflater @@ -24,7 +22,6 @@ import android.view.MenuInflater import android.view.MenuItem import android.view.View import android.view.ViewGroup -import android.view.ViewPropertyAnimator import android.view.WindowInsets import android.view.animation.DecelerateInterpolator import android.view.inputmethod.InputMethodManager @@ -41,16 +38,16 @@ import androidx.transition.ChangeBounds import androidx.transition.ChangeImageTransform import androidx.transition.TransitionManager import androidx.transition.TransitionSet +import coil.Coil +import coil.api.clear +import coil.api.loadAny +import coil.request.LoadRequest import com.afollestad.materialdialogs.MaterialDialog import com.afollestad.materialdialogs.checkbox.checkBoxPrompt import com.afollestad.materialdialogs.checkbox.isCheckPromptChecked import com.afollestad.materialdialogs.list.listItems import com.bluelinelabs.conductor.ControllerChangeHandler import com.bluelinelabs.conductor.ControllerChangeType -import com.bumptech.glide.load.engine.DiskCacheStrategy -import com.bumptech.glide.request.target.CustomTarget -import com.bumptech.glide.request.transition.Transition -import com.bumptech.glide.signature.ObjectKey import com.google.android.material.snackbar.BaseTransientBottomBar import com.google.android.material.snackbar.Snackbar import eu.davidea.flexibleadapter.FlexibleAdapter @@ -60,10 +57,8 @@ import eu.kanade.tachiyomi.data.database.DatabaseHelper import eu.kanade.tachiyomi.data.database.models.Category import eu.kanade.tachiyomi.data.database.models.Chapter import eu.kanade.tachiyomi.data.database.models.Manga -import eu.kanade.tachiyomi.data.database.models.MangaImpl import eu.kanade.tachiyomi.data.download.DownloadService import eu.kanade.tachiyomi.data.download.model.Download -import eu.kanade.tachiyomi.data.glide.GlideApp import eu.kanade.tachiyomi.data.notification.NotificationReceiver import eu.kanade.tachiyomi.data.preference.getOrDefault import eu.kanade.tachiyomi.data.track.model.TrackSearch @@ -165,13 +160,10 @@ class MangaDetailsController : BaseController, var toolbarIsColored = false private var snack: Snackbar? = null val fromCatalogue = args.getBoolean(FROM_CATALOGUE_EXTRA, false) - var coverDrawable: Drawable? = null private var trackingBottomSheet: TrackingBottomSheet? = null private var startingDLChapterPos: Int? = null private var editMangaDialog: EditMangaDialog? = null var refreshTracker: Int? = null - private var textAnim: ViewPropertyAnimator? = null - private var scrollAnim: ViewPropertyAnimator? = null var chapterPopupMenu: Pair? = null private var query = "" @@ -313,42 +305,39 @@ class MangaDetailsController : BaseController, } } - /** Get the color of the manga cover based on the current theme */ + /** Get the color of the manga cover*/ fun setPaletteColor() { val view = view ?: return coverColor = null - GlideApp.with(view.context).load(manga).diskCacheStrategy(DiskCacheStrategy.RESOURCE) - .signature(ObjectKey(MangaImpl.getLastCoverFetch(manga!!.id!!).toString())) - .into(object : CustomTarget() { - override fun onResourceReady( - resource: Drawable, - transition: Transition? - ) { - coverDrawable = resource - val bitmapCover = resource as? BitmapDrawable ?: return - Palette.from(bitmapCover.bitmap).generate { - if (recycler == null || it == null) return@generate - val colorBack = view.context.getResourceColor( - android.R.attr.colorBackground - ) - val backDropColor = if (!view.context.isInNightMode()) { - it.getLightVibrantColor(colorBack) - } else { - it.getDarkVibrantColor(colorBack) - } - coverColor = backDropColor - getHeader()?.setBackDrop(backDropColor) - if (toolbarIsColored) { - val translucentColor = ColorUtils.setAlphaComponent(backDropColor, 175) - (activity as MainActivity).toolbar.setBackgroundColor(translucentColor) - activity?.window?.statusBarColor = translucentColor - } - } - getHeader()?.updateCover(presenter.manga) - } - override fun onLoadCleared(placeholder: Drawable?) {} - }) + val request = LoadRequest.Builder(view.context).data(manga).allowHardware(false) + .target { drawable -> + val bitmap = (drawable as BitmapDrawable).bitmap + // Generate the Palette on a background thread. + Palette.from(bitmap).generate { + if (recycler == null || it == null) return@generate + val colorBack = view.context.getResourceColor( + android.R.attr.colorBackground + ) + // this makes the color more consistent regardless of theme + val backDropColor = ColorUtils.blendARGB(it.getVibrantColor(colorBack), colorBack, .35f) + + coverColor = backDropColor + getHeader()?.setBackDrop(backDropColor) + if (toolbarIsColored) { + val translucentColor = ColorUtils.setAlphaComponent(backDropColor, 175) + (activity as MainActivity).toolbar.setBackgroundColor(translucentColor) + activity?.window?.statusBarColor = translucentColor + } + } + }.build() + Coil.imageLoader(view.context).execute(request) + } + + fun resetCovers() { + manga_cover_full.clear() + manga_cover_full.loadAny(manga) + getHeader()?.updateCover(manga!!, true) } /** Set toolbar theme for themes that are inverted (ie. light blue theme) */ @@ -404,12 +393,15 @@ class MangaDetailsController : BaseController, super.onActivityResumed(activity) presenter.isLockedFromSearch = SecureActivityDelegate.shouldBeLocked() presenter.headerItem.isLocked = presenter.isLockedFromSearch + manga!!.thumbnail_url = presenter.refreshMangaFromDb().thumbnail_url presenter.fetchChapters(refreshTracker == null) if (refreshTracker != null) { trackingBottomSheet?.refreshItem(refreshTracker ?: 0) presenter.refreshTracking() refreshTracker = null } + // reset the covers and palette cause user might have set a custom cover + presenter.forceUpdateCovers(false) val isCurrentController = router?.backstack?.lastOrNull()?.controller() == this if (isCurrentController) { @@ -774,7 +766,7 @@ class MangaDetailsController : BaseController, ), waitForPositiveButton = false, selection = { _, index, _ -> when (index) { 0 -> changeCover() - else -> presenter.clearCover() + else -> presenter.clearCustomCover() } }).show() } else { @@ -810,20 +802,16 @@ class MangaDetailsController : BaseController, //endregion override fun prepareToShareManga() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && coverDrawable != null) - GlideApp.with(activity!!).asBitmap().load(presenter.manga).into(object : - CustomTarget() { - override fun onResourceReady(resource: Bitmap, transition: Transition?) { - presenter.shareManga(resource) - } - - override fun onLoadCleared(placeholder: Drawable?) {} - - override fun onLoadFailed(errorDrawable: Drawable?) { - shareManga() - } - }) - else shareManga() + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + val request = LoadRequest.Builder(activity!!).data(manga).target(onError = { + shareManga() + }, onSuccess = { + presenter.shareManga((it as BitmapDrawable).bitmap) + }).build() + Coil.imageLoader(activity!!).execute(request) + } else { + shareManga() + } } fun shareManga(cover: File? = null) { @@ -1257,7 +1245,6 @@ class MangaDetailsController : BaseController, if (editMangaDialog != null) editMangaDialog?.updateCover(uri) else { presenter.editCoverWithStream(uri) - setPaletteColor() } } catch (error: IOException) { activity.toast(R.string.failed_to_update_cover) @@ -1271,10 +1258,9 @@ class MangaDetailsController : BaseController, currentAnimator?.cancel() // Load the high-resolution "zoomed-in" image. + manga_cover_full?.loadAny(manga) val expandedImageView = manga_cover_full ?: return val fullBackdrop = full_backdrop - val image = coverDrawable ?: return - expandedImageView.setImageDrawable(image) // Hide the thumbnail and show the zoomed-in view. When the animation // begins, it will position the zoomed-in view in the place of the diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaDetailsPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaDetailsPresenter.kt index 47bfb94278..d4204cfd7a 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaDetailsPresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaDetailsPresenter.kt @@ -11,7 +11,6 @@ import eu.kanade.tachiyomi.data.database.models.Chapter import eu.kanade.tachiyomi.data.database.models.LibraryManga import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.MangaCategory -import eu.kanade.tachiyomi.data.database.models.MangaImpl import eu.kanade.tachiyomi.data.database.models.Track import eu.kanade.tachiyomi.data.download.DownloadManager import eu.kanade.tachiyomi.data.download.model.Download @@ -27,6 +26,7 @@ import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.source.fetchChapterListAsync import eu.kanade.tachiyomi.source.fetchMangaDetailsAsync import eu.kanade.tachiyomi.source.model.SChapter +import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.ui.manga.chapter.ChapterItem import eu.kanade.tachiyomi.ui.manga.track.TrackItem import eu.kanade.tachiyomi.ui.security.SecureActivityDelegate @@ -368,6 +368,12 @@ class MangaDetailsPresenter( if (update) controller.updateChapters(this.chapters) } + fun refreshMangaFromDb(): Manga { + val dbManga = db.getManga(manga.id!!).executeAsBlocking() + manga.copyFrom(dbManga!!) + return dbManga + } + /** Refresh Manga Info and Chapter List (not tracking) */ fun refreshAll() { if (controller.isNotOnline() && manga.source != LocalSource.ID) return @@ -398,12 +404,15 @@ class MangaDetailsPresenter( if (networkManga != null) { manga.copyFrom(networkManga) manga.initialized = true - db.insertManga(manga).executeAsBlocking() - if (thumbnailUrl != networkManga.thumbnail_url && !manga.hasCustomCover()) { - coverCache.deleteFromCache(thumbnailUrl) - MangaImpl.setLastCoverFetch(manga.id!!, Date().time) - withContext(Dispatchers.Main) { controller.setPaletteColor() } + + if (shouldUpdateCover(thumbnailUrl, networkManga)) { + coverCache.deleteFromCache(manga, false) + manga.thumbnail_url = networkManga.thumbnail_url + withContext(Dispatchers.Main) { + forceUpdateCovers() + } } + db.insertManga(manga).executeAsBlocking() } val finChapters = chapters.await() if (finChapters.isNotEmpty()) { @@ -451,6 +460,19 @@ class MangaDetailsPresenter( } } + private fun shouldUpdateCover(thumbnailUrl: String?, networkManga: SManga): Boolean { + val refreshCovers = preferences.refreshCoversToo().getOrDefault() + if (thumbnailUrl == networkManga.thumbnail_url && !refreshCovers) { + return false + } + if (thumbnailUrl != networkManga.thumbnail_url && !manga.hasCustomCover()) { + return true + } + if (manga.hasCustomCover()) return false + + return refreshCovers + } + /** * Requests an updated list of chapters from the source. */ @@ -588,7 +610,9 @@ class MangaDetailsPresenter( manga.favorite = !manga.favorite when (manga.favorite) { - true -> manga.date_added = Date().time + true -> { + manga.date_added = Date().time + } false -> manga.date_added = 0 } @@ -639,7 +663,7 @@ class MangaDetailsPresenter( } fun confirmDeletion() { - coverCache.deleteFromCache(manga.thumbnail_url) + coverCache.deleteFromCache(manga) db.resetMangaInfo(manga).executeAsBlocking() downloadManager.deleteManga(manga, source) asyncUpdateMangaAndChapters(true) @@ -707,36 +731,41 @@ class MangaDetailsPresenter( db.updateMangaInfo(manga).executeAsBlocking() } if (uri != null) editCoverWithStream(uri) - controller.updateHeader() } - fun clearCover() { + /** + * Remvoe custom cover + */ + fun clearCustomCover() { if (manga.hasCustomCover()) { - coverCache.deleteFromCache(manga.thumbnail_url!!) - manga.thumbnail_url = manga.thumbnail_url?.removePrefix("Custom-") + coverCache.deleteFromCache(manga) + manga.removeCustomThumbnailUrl() db.insertManga(manga).executeAsBlocking() - MangaImpl.setLastCoverFetch(manga.id!!, Date().time) - controller.updateHeader() - controller.setPaletteColor() + forceUpdateCovers() } } + fun forceUpdateCovers(deleteCache: Boolean = true) { + if (deleteCache) coverCache.deleteFromCache(manga) + controller.setPaletteColor() + controller.resetCovers() + } + fun editCoverWithStream(uri: Uri): Boolean { val inputStream = downloadManager.context.contentResolver.openInputStream(uri) ?: return false if (manga.source == LocalSource.ID) { LocalSource.updateCover(downloadManager.context, manga, inputStream) - MangaImpl.setLastCoverFetch(manga.id!!, Date().time) + forceUpdateCovers() return true } if (manga.favorite) { - if (!manga.hasCustomCover()) { - manga.thumbnail_url = "Custom-${manga.thumbnail_url ?: manga.id!!}" - db.insertManga(manga).executeAsBlocking() - } - coverCache.copyToCache(manga.thumbnail_url!!, inputStream) - MangaImpl.setLastCoverFetch(manga.id!!, Date().time) + coverCache.deleteFromCache(manga) + manga.setCustomThumbnailUrl() + db.insertManga(manga).executeAsBlocking() + coverCache.copyToCache(manga, inputStream) + forceUpdateCovers(false) return true } return false diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaHeaderHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaHeaderHolder.kt index 46c6d274a7..cec007dbc0 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaHeaderHolder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaHeaderHolder.kt @@ -9,14 +9,11 @@ import android.view.ViewGroup import androidx.constraintlayout.widget.ConstraintLayout import androidx.core.content.ContextCompat import androidx.core.graphics.ColorUtils -import com.bumptech.glide.load.engine.DiskCacheStrategy -import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions -import com.bumptech.glide.signature.ObjectKey +import coil.api.clear +import coil.api.loadAny import com.google.android.material.button.MaterialButton import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.database.models.Manga -import eu.kanade.tachiyomi.data.database.models.MangaImpl -import eu.kanade.tachiyomi.data.glide.GlideApp import eu.kanade.tachiyomi.source.LocalSource import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder @@ -287,11 +284,15 @@ class MangaHeaderHolder( val presenter = adapter.delegate.mangaPresenter() val tracked = presenter.isTracked() with(track_button) { - text = itemView.context.getString(if (tracked) R.string.tracked - else R.string.tracking) + text = itemView.context.getString( + if (tracked) R.string.tracked + else R.string.tracking + ) - icon = ContextCompat.getDrawable(itemView.context, if (tracked) R.drawable - .ic_check_white_24dp else R.drawable.ic_sync_black_24dp) + icon = ContextCompat.getDrawable( + itemView.context, if (tracked) R.drawable + .ic_check_white_24dp else R.drawable.ic_sync_black_24dp + ) checked(tracked) } } @@ -307,22 +308,18 @@ class MangaHeaderHolder( } } - fun updateCover(manga: Manga) { - if (!isCached(manga)) return - GlideApp.with(view.context).load(manga).diskCacheStrategy(DiskCacheStrategy.RESOURCE) - .signature(ObjectKey(MangaImpl.getLastCoverFetch(manga.id!!).toString())) - .into(manga_cover) - GlideApp.with(view.context).load(manga).diskCacheStrategy(DiskCacheStrategy.RESOURCE) - .signature(ObjectKey(MangaImpl.getLastCoverFetch(manga.id!!).toString())).centerCrop() - .transition(DrawableTransitionOptions.withCrossFade()).into(backdrop) + fun updateCover(manga: Manga, forceUpdate: Boolean = false) { + if (!isCached(manga) && !forceUpdate) return + manga_cover.clear() + backdrop.clear() + manga_cover.loadAny(manga) + backdrop.loadAny(manga) } private fun isCached(manga: Manga): Boolean { if (manga.source == LocalSource.ID) return true - val coverCache = adapter.delegate.mangaPresenter().coverCache manga.thumbnail_url?.let { - return if (manga.favorite) coverCache.getCoverFile(it).exists() - else true + return adapter.delegate.mangaPresenter().coverCache.getCoverFile(manga).exists() } return manga.initialized } 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 e5a0c5966c..a33616d57a 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 @@ -4,10 +4,9 @@ import android.content.Context import android.view.View import android.view.ViewGroup import android.widget.ArrayAdapter -import com.bumptech.glide.load.engine.DiskCacheStrategy -import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions +import coil.api.clear +import coil.api.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.util.view.gone import eu.kanade.tachiyomi.util.view.inflate @@ -46,12 +45,9 @@ class TrackSearchAdapter(context: Context) : fun onSetValues(track: TrackSearch) { view.track_search_title.text = track.title view.track_search_summary.text = track.summary - GlideApp.with(view.context).clear(view.track_search_cover) + view.track_search_cover.clear() if (!track.cover_url.isNullOrEmpty()) { - GlideApp.with(view.context).load(track.cover_url) - .diskCacheStrategy(DiskCacheStrategy.RESOURCE).centerCrop() - .transition(DrawableTransitionOptions.withCrossFade()) - .into(view.track_search_cover) + view.track_search_cover.load(track.cover_url) } if (track.publishing_status.isNullOrBlank()) { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/migration/MangaHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/migration/MangaHolder.kt index 92a4f91028..106d18ba09 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/migration/MangaHolder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/migration/MangaHolder.kt @@ -2,10 +2,10 @@ package eu.kanade.tachiyomi.ui.migration import android.view.View import androidx.recyclerview.widget.RecyclerView -import com.bumptech.glide.load.engine.DiskCacheStrategy +import coil.api.clear +import coil.api.loadAny import eu.davidea.flexibleadapter.FlexibleAdapter import eu.davidea.flexibleadapter.items.IFlexible -import eu.kanade.tachiyomi.data.glide.GlideApp import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder import kotlinx.android.synthetic.main.manga_list_item.* @@ -20,9 +20,7 @@ class MangaHolder( subtitle.text = item.manga.author?.trim() // Update the cover. - GlideApp.with(itemView.context).clear(cover_thumbnail) - GlideApp.with(itemView.context).load(item.manga) - .diskCacheStrategy(DiskCacheStrategy.RESOURCE).centerCrop().dontAnimate() - .into(cover_thumbnail) + cover_thumbnail.clear() + cover_thumbnail.loadAny(item.manga) } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/migration/manga/process/MigrationProcessHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/migration/manga/process/MigrationProcessHolder.kt index 882192570c..cbfead37c0 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/migration/manga/process/MigrationProcessHolder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/migration/manga/process/MigrationProcessHolder.kt @@ -3,12 +3,11 @@ package eu.kanade.tachiyomi.ui.migration.manga.process import android.view.View import androidx.appcompat.widget.PopupMenu import androidx.constraintlayout.widget.ConstraintLayout -import com.bumptech.glide.load.engine.DiskCacheStrategy -import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions +import coil.Coil +import coil.request.LoadRequest import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.database.DatabaseHelper import eu.kanade.tachiyomi.data.database.models.Manga -import eu.kanade.tachiyomi.data.glide.GlideApp import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.source.SourceManager import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder @@ -20,7 +19,7 @@ import eu.kanade.tachiyomi.util.view.invisible import eu.kanade.tachiyomi.util.view.setVectorCompat import eu.kanade.tachiyomi.util.view.visible import eu.kanade.tachiyomi.util.view.withFadeTransaction -import eu.kanade.tachiyomi.widget.StateImageViewTarget +import eu.kanade.tachiyomi.widget.CoverViewTarget import kotlinx.android.synthetic.main.manga_grid_item.view.* import kotlinx.android.synthetic.main.migration_process_item.* import kotlinx.android.synthetic.main.unread_download_badge.view.* @@ -133,14 +132,10 @@ class MigrationProcessHolder( private fun View.attachManga(manga: Manga, source: Source, isTo: Boolean) { (layoutParams as ConstraintLayout.LayoutParams).verticalBias = 1f progress.gone() - GlideApp.with(view.context.applicationContext).load(manga).apply { - diskCacheStrategy(DiskCacheStrategy.RESOURCE) - if (isTo) { - transition(DrawableTransitionOptions.withCrossFade()) - .into(StateImageViewTarget(cover_thumbnail, progress)) - } else - into(cover_thumbnail) - } + + val request = LoadRequest.Builder(view.context).data(manga) + .target(CoverViewTarget(cover_thumbnail, progress)).build() + Coil.imageLoader(view.context).execute(request) compact_title.visible() gradient.visible() @@ -164,11 +159,15 @@ class MigrationProcessHolder( val latestChapter = mangaChapters.maxBy { it.chapter_number }?.chapter_number ?: -1f if (latestChapter > 0f) { - subtitle.text = context.getString(R.string.latest_, - DecimalFormat("#.#").format(latestChapter)) + subtitle.text = context.getString( + R.string.latest_, + DecimalFormat("#.#").format(latestChapter) + ) } else { - subtitle.text = context.getString(R.string.latest_, - context.getString(R.string.unknown)) + subtitle.text = context.getString( + R.string.latest_, + context.getString(R.string.unknown) + ) } } 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 52309e9f8b..891983a25e 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 @@ -10,7 +10,6 @@ import eu.kanade.tachiyomi.data.database.DatabaseHelper import eu.kanade.tachiyomi.data.database.models.Chapter import eu.kanade.tachiyomi.data.database.models.History import eu.kanade.tachiyomi.data.database.models.Manga -import eu.kanade.tachiyomi.data.database.models.MangaImpl import eu.kanade.tachiyomi.data.download.DownloadManager import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.getOrDefault @@ -536,18 +535,17 @@ class ReaderPresenter( .fromCallable { if (manga.source == LocalSource.ID) { val context = Injekt.get() + coverCache.deleteFromCache(manga) LocalSource.updateCover(context, manga, stream()) R.string.cover_updated SetAsCoverResult.Success } else { - val thumbUrl = manga.thumbnail_url ?: throw Exception("Image url not found") + manga.thumbnail_url ?: throw Exception("Image url not found") if (manga.favorite) { - if (!manga.hasCustomCover()) { - manga.thumbnail_url = "Custom-${manga.thumbnail_url ?: manga.id!!}" - db.insertManga(manga).executeAsBlocking() - } - coverCache.copyToCache(manga.thumbnail_url!!, stream()) - MangaImpl.setLastCoverFetch(manga.id!!, Date().time) + coverCache.deleteFromCache(manga) + manga.setCustomThumbnailUrl() + db.insertManga(manga).executeAsBlocking() + coverCache.copyToCache(manga, stream()) 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 4f35424802..9d01b277a1 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,11 +2,13 @@ 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 androidx.core.content.ContextCompat -import com.bumptech.glide.load.engine.DiskCacheStrategy +import coil.Coil +import coil.request.CachePolicy +import coil.request.LoadRequest 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 @@ -36,19 +38,19 @@ class SaveImageNotifier(private val context: Context) { * @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 = LoadRequest.Builder(context).memoryCachePolicy(CachePolicy.DISABLED).diskCachePolicy(CachePolicy.DISABLED) + .data(file) + .size(720, 1280) + .target(onSuccess = { + val bitmap = (it as BitmapDrawable).bitmap + if (bitmap != null) { + showCompleteNotification(file, bitmap) + } else { + onError(null) + } + }).build() + Coil.imageLoader(context).execute(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 651e6dd42e..7a9213da06 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 @@ -18,19 +18,12 @@ import android.widget.FrameLayout import android.widget.ImageView import android.widget.LinearLayout import android.widget.TextView -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.api.loadAny +import coil.request.CachePolicy 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.data.preference.PreferencesHelper import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.ui.reader.model.ReaderPage @@ -43,6 +36,7 @@ import eu.kanade.tachiyomi.util.system.isInNightMode import eu.kanade.tachiyomi.util.system.launchUI import eu.kanade.tachiyomi.util.view.gone import eu.kanade.tachiyomi.util.view.visible +import eu.kanade.tachiyomi.widget.GifViewTarget import eu.kanade.tachiyomi.widget.ViewPagerAdapter import kotlinx.coroutines.Dispatchers.Default import kotlinx.coroutines.withContext @@ -487,37 +481,11 @@ 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 - } - }) - .into(this) + this.loadAny(stream.readBytes()) { + memoryCachePolicy(CachePolicy.DISABLED) + diskCachePolicy(CachePolicy.DISABLED) + target(GifViewTarget(this@setImage, progressBar, decodeErrorLayout)) + } } companion object { 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 4721e6811f..16aa5454a7 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 @@ -4,7 +4,6 @@ import android.annotation.SuppressLint import android.content.Intent import android.content.res.Resources import android.graphics.Color -import android.graphics.drawable.Drawable import android.net.Uri import android.view.Gravity import android.view.ViewGroup @@ -16,18 +15,12 @@ import android.widget.LinearLayout import android.widget.TextView import androidx.appcompat.widget.AppCompatButton import androidx.appcompat.widget.AppCompatImageView -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.api.clear +import coil.api.loadAny +import coil.request.CachePolicy 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 @@ -36,6 +29,7 @@ import eu.kanade.tachiyomi.util.system.dpToPx import eu.kanade.tachiyomi.util.view.gone import eu.kanade.tachiyomi.util.view.updatePaddingRelative import eu.kanade.tachiyomi.util.view.visible +import eu.kanade.tachiyomi.widget.GifViewTarget import rx.Observable import rx.Subscription import rx.android.schedulers.AndroidSchedulers @@ -149,7 +143,7 @@ class WebtoonPageHolder( removeDecodeErrorLayout() subsamplingImageView?.recycle() subsamplingImageView?.gone() - imageView?.let { GlideApp.with(frame).clear(it) } + imageView?.clear() imageView?.gone() progressBar.setProgress(0) } @@ -491,36 +485,10 @@ 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 - } - }) - .into(this) + this.loadAny(stream.readBytes()) { + memoryCachePolicy(CachePolicy.DISABLED) + diskCachePolicy(CachePolicy.DISABLED) + target(GifViewTarget(this@setImage, progressBar, decodeErrorLayout)) + } } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/recent_updates/RecentChapterHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/recent_updates/RecentChapterHolder.kt index b24a3ab0e4..680a4a824c 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/recent_updates/RecentChapterHolder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/recent_updates/RecentChapterHolder.kt @@ -3,9 +3,10 @@ package eu.kanade.tachiyomi.ui.recent_updates import android.app.Activity import android.view.View import androidx.core.content.ContextCompat -import com.bumptech.glide.load.engine.DiskCacheStrategy +import coil.api.clear +import coil.api.loadAny +import coil.transform.CircleCropTransformation import eu.kanade.tachiyomi.R -import eu.kanade.tachiyomi.data.glide.GlideApp import eu.kanade.tachiyomi.ui.manga.chapter.BaseChapterHolder import eu.kanade.tachiyomi.util.chapter.ChapterUtil import eu.kanade.tachiyomi.util.system.getResourceColor @@ -71,10 +72,9 @@ class RecentChapterHolder(private val view: View, private val adapter: RecentCha // Set cover if ((view.context as? Activity)?.isDestroyed != true) { - GlideApp.with(itemView.context).clear(manga_cover) - if (!item.manga.thumbnail_url.isNullOrEmpty()) { - GlideApp.with(itemView.context).load(item.manga) - .diskCacheStrategy(DiskCacheStrategy.RESOURCE).circleCrop().into(manga_cover) + manga_cover.clear() + manga_cover.loadAny(item.manga) { + transformations(CircleCropTransformation()) } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/recently_read/RecentlyReadHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/recently_read/RecentlyReadHolder.kt index 78cea18195..c18d040fec 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/recently_read/RecentlyReadHolder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/recently_read/RecentlyReadHolder.kt @@ -1,10 +1,10 @@ package eu.kanade.tachiyomi.ui.recently_read import android.view.View -import com.bumptech.glide.load.engine.DiskCacheStrategy +import coil.api.clear +import coil.api.loadAny import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.database.models.MangaChapterHistory -import eu.kanade.tachiyomi.data.glide.GlideApp import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder import eu.kanade.tachiyomi.util.lang.toTimestampString import kotlinx.android.synthetic.main.recently_read_item.* @@ -59,13 +59,7 @@ class RecentlyReadHolder( last_read.text = Date(history.last_read).toTimestampString(adapter.dateFormat) // Set cover - GlideApp.with(itemView.context).clear(cover) - if (!manga.thumbnail_url.isNullOrEmpty()) { - GlideApp.with(itemView.context) - .load(manga) - .diskCacheStrategy(DiskCacheStrategy.RESOURCE) - .centerCrop() - .into(cover) - } + cover.clear() + cover.loadAny(manga) } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/recents/RecentMangaHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/recents/RecentMangaHolder.kt index 729a9acb1c..97d6d552f5 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/recents/RecentMangaHolder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/recents/RecentMangaHolder.kt @@ -2,12 +2,9 @@ package eu.kanade.tachiyomi.ui.recents import android.app.Activity import android.view.View -import com.bumptech.glide.load.engine.DiskCacheStrategy -import com.bumptech.glide.signature.ObjectKey +import coil.api.loadAny import eu.kanade.tachiyomi.R -import eu.kanade.tachiyomi.data.database.models.MangaImpl import eu.kanade.tachiyomi.data.download.model.Download -import eu.kanade.tachiyomi.data.glide.GlideApp import eu.kanade.tachiyomi.source.LocalSource import eu.kanade.tachiyomi.ui.manga.chapter.BaseChapterHolder import eu.kanade.tachiyomi.util.chapter.ChapterUtil @@ -72,10 +69,7 @@ class RecentMangaHolder( ) } if ((itemView.context as? Activity)?.isDestroyed != true) { - GlideApp.with(itemView.context).load(item.mch.manga).diskCacheStrategy( - DiskCacheStrategy.RESOURCE - ).signature(ObjectKey(MangaImpl.getLastCoverFetch(item.mch.manga.id!!).toString())) - .into(cover_thumbnail) + cover_thumbnail.loadAny(item.mch.manga) } notifyStatus( if (adapter.isSelected(adapterPosition)) Download.CHECKED else item.status, diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsAdvancedController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsAdvancedController.kt index 37fc0bb6a9..dd39b4ed17 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsAdvancedController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsAdvancedController.kt @@ -63,6 +63,16 @@ class SettingsAdvancedController : SettingsController() { onClick { clearChapterCache() } } + + preference { + titleRes = R.string.clean_up_cached_covers + summary = context.getString(R.string.delete_old_covers_in_library_used_, coverCache.getChapterCacheSize()) + + onClick { + context.toast(R.string.starting_cleanup) + coverCache.deleteOldCovers() + } + } preference { titleRes = R.string.clear_cookies @@ -101,16 +111,6 @@ class SettingsAdvancedController : SettingsController() { onClick { cleanupDownloads() } } - preference { - titleRes = R.string.clean_up_cached_covers - - summaryRes = R.string.delete_old_covers_in_library - - onClick { - context.toast(R.string.starting_cleanup) - coverCache.deleteOldCovers() - } - } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { val pm = context.getSystemService(Context.POWER_SERVICE) as? PowerManager? if (pm != null) preference { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/source/browse/BrowseSourceGridHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/source/browse/BrowseSourceGridHolder.kt index 0480783109..f13dedb2fc 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/source/browse/BrowseSourceGridHolder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/source/browse/BrowseSourceGridHolder.kt @@ -3,17 +3,18 @@ package eu.kanade.tachiyomi.ui.source.browse import android.app.Activity import android.view.View import androidx.recyclerview.widget.RecyclerView -import com.bumptech.glide.Glide -import com.bumptech.glide.load.engine.DiskCacheStrategy -import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions +import coil.Coil +import coil.api.clear +import coil.request.LoadRequest import eu.davidea.flexibleadapter.FlexibleAdapter import eu.davidea.flexibleadapter.items.IFlexible import eu.kanade.tachiyomi.data.database.models.Manga -import eu.kanade.tachiyomi.data.glide.GlideApp import eu.kanade.tachiyomi.ui.library.LibraryCategoryAdapter import eu.kanade.tachiyomi.util.view.gone -import eu.kanade.tachiyomi.widget.StateImageViewTarget +import eu.kanade.tachiyomi.widget.CoverViewTarget import kotlinx.android.synthetic.main.manga_grid_item.* +import kotlinx.android.synthetic.main.manga_grid_item.cover_thumbnail +import kotlinx.android.synthetic.main.manga_grid_item.title import kotlinx.android.synthetic.main.unread_download_badge.* /** @@ -58,16 +59,12 @@ class BrowseSourceGridHolder( override fun setImage(manga: Manga) { if ((view.context as? Activity)?.isDestroyed == true) return - if (manga.thumbnail_url == null) - Glide.with(view.context).clear(cover_thumbnail) - else { - GlideApp.with(view.context) - .load(manga) - .diskCacheStrategy(DiskCacheStrategy.DATA) - .centerCrop() - .placeholder(android.R.color.transparent) - .transition(DrawableTransitionOptions.withCrossFade()) - .into(StateImageViewTarget(cover_thumbnail, progress)) + if (manga.thumbnail_url == null) { + cover_thumbnail.clear() + } else { + val id = manga.id ?: return + val request = LoadRequest.Builder(view.context).data(manga).target(CoverViewTarget(cover_thumbnail, progress)).build() + Coil.imageLoader(view.context).execute(request) } } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/source/browse/BrowseSourceListHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/source/browse/BrowseSourceListHolder.kt index 476fff6352..0b0ad79027 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/source/browse/BrowseSourceListHolder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/source/browse/BrowseSourceListHolder.kt @@ -2,15 +2,16 @@ package eu.kanade.tachiyomi.ui.source.browse import android.view.View import androidx.recyclerview.widget.RecyclerView -import com.bumptech.glide.load.engine.DiskCacheStrategy -import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions +import coil.Coil +import coil.api.clear +import coil.request.LoadRequest +import coil.transform.RoundedCornersTransformation import eu.davidea.flexibleadapter.FlexibleAdapter import eu.davidea.flexibleadapter.items.IFlexible import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.database.models.Manga -import eu.kanade.tachiyomi.data.glide.GlideApp import eu.kanade.tachiyomi.util.system.getResourceColor -import eu.kanade.tachiyomi.widget.StateImageViewTarget +import eu.kanade.tachiyomi.widget.CoverViewTarget import kotlinx.android.synthetic.main.manga_list_item.* /** @@ -42,17 +43,14 @@ class BrowseSourceListHolder(private val view: View, adapter: FlexibleAdapter(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?.visible() - super.onLoadStarted(placeholder) - } - - override fun onLoadFailed(errorDrawable: Drawable?) { - progress?.gone() - view.scaleType = errorScaleType - - val vector = VectorDrawableCompat.create(view.context.resources, errorDrawableRes, null) - vector?.setTint(view.context.getResourceColor(android.R.attr.textColorSecondary)) - view.setImageDrawable(vector) - } - - override fun onLoadCleared(placeholder: Drawable?) { - progress?.gone() - super.onLoadCleared(placeholder) - } - - override fun onResourceReady(resource: Drawable, transition: Transition?) { - progress?.gone() - view.scaleType = imageScaleType - super.onResourceReady(resource, transition) - this.resource = resource - } -} diff --git a/app/src/main/res/layout/manga_grid_item.xml b/app/src/main/res/layout/manga_grid_item.xml index d1c74c90f5..49f98a3b2a 100644 --- a/app/src/main/res/layout/manga_grid_item.xml +++ b/app/src/main/res/layout/manga_grid_item.xml @@ -34,9 +34,8 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:adjustViewBounds="true" + android:scaleType="centerCrop" android:background="?android:attr/colorBackground" - android:maxHeight="250dp" - tools:background="?android:attr/colorBackground" tools:ignore="ContentDescription" tools:src="@mipmap/ic_launcher" /> diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index ad4ec42666..342547cbd9 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -540,8 +540,8 @@ Cleanup done. Removed %d folders Clean up cached covers - Delete old and unused cached covers of - manga in your library that has been updated + Delete old and unused cached covers of + manga in your library that has been updated.\nCurrently using: %1$s Version