diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 902b97ccaa..56595346fa 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -200,7 +200,7 @@ dependencies { implementation("com.github.inorichi.injekt:injekt-core:65b0440") // Image library - val coilVersion = "1.3.2" + val coilVersion = "2.0.0-rc03" implementation("io.coil-kt:coil:$coilVersion") implementation("io.coil-kt:coil-gif:$coilVersion") implementation("io.coil-kt:coil-svg:$coilVersion") @@ -270,6 +270,7 @@ tasks { "-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi", "-opt-in=kotlinx.coroutines.InternalCoroutinesApi", "-opt-in=kotlinx.serialization.ExperimentalSerializationApi", + "-opt-in=coil.annotation.ExperimentalCoilApi", ) } 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 39ce25ade9..3a905aa254 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 @@ -112,7 +112,7 @@ class CoverCache(val context: Context) { ), ) } - context.imageLoader.memoryCache.clear() + context.imageLoader.memoryCache?.clear() lastClean = System.currentTimeMillis() } @@ -169,7 +169,7 @@ class CoverCache(val context: Context) { fun setCustomCoverToCache(manga: Manga, inputStream: InputStream) { getCustomCoverFile(manga).outputStream().use { inputStream.copyTo(it) - context.imageLoader.memoryCache.remove(MemoryCache.Key(manga.key())) + context.imageLoader.memoryCache?.remove(MemoryCache.Key(manga.key())) } } @@ -183,7 +183,7 @@ class CoverCache(val context: Context) { val result = getCustomCoverFile(manga).let { it.exists() && it.delete() } - context.imageLoader.memoryCache.remove(MemoryCache.Key(manga.key())) + context.imageLoader.memoryCache?.remove(MemoryCache.Key(manga.key())) return result } @@ -205,7 +205,7 @@ class CoverCache(val context: Context) { fun deleteFromCache(name: String?) { if (name.isNullOrEmpty()) return val file = getCoverFile(MangaImpl().apply { thumbnail_url = name }) - context.imageLoader.memoryCache.remove(MemoryCache.Key(file.name)) + context.imageLoader.memoryCache?.remove(MemoryCache.Key(file.name)) if (file.exists()) file.delete() } @@ -226,7 +226,7 @@ class CoverCache(val context: Context) { val file = getCoverFile(manga) if (deleteCustom) deleteCustomCover(manga) if (file.exists()) { - context.imageLoader.memoryCache.remove(MemoryCache.Key(manga.key())) + context.imageLoader.memoryCache?.remove(MemoryCache.Key(manga.key())) file.delete() } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/image/coil/ByteArrayFetcher.kt b/app/src/main/java/eu/kanade/tachiyomi/data/image/coil/ByteArrayFetcher.kt deleted file mode 100644 index 24c1b37191..0000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/data/image/coil/ByteArrayFetcher.kt +++ /dev/null @@ -1,30 +0,0 @@ -package eu.kanade.tachiyomi.data.image.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 - -class ByteArrayFetcher : Fetcher { - - override fun key(data: ByteArray): String? = null - - override suspend fun fetch( - pool: BitmapPool, - data: ByteArray, - size: Size, - options: Options, - ): FetchResult { - return SourceResult( - source = ByteArrayInputStream(data).source().buffer(), - mimeType = "image/gif", - dataSource = DataSource.MEMORY, - ) - } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/image/coil/CoilSetup.kt b/app/src/main/java/eu/kanade/tachiyomi/data/image/coil/CoilSetup.kt index d3d2173913..b6894cf0b9 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/image/coil/CoilSetup.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/image/coil/CoilSetup.kt @@ -3,37 +3,60 @@ package eu.kanade.tachiyomi.data.image.coil import android.app.ActivityManager import android.content.Context import android.os.Build -import androidx.core.content.ContextCompat.getSystemService import androidx.core.content.getSystemService import coil.Coil import coil.ImageLoader import coil.decode.GifDecoder import coil.decode.ImageDecoderDecoder -import coil.decode.SvgDecoder +import coil.disk.DiskCache +import coil.memory.MemoryCache import eu.kanade.tachiyomi.network.NetworkHelper import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get class CoilSetup(context: Context) { init { - val imageLoader = ImageLoader.Builder(context) - .availableMemoryPercentage(0.40) - .crossfade(true) - .allowRgb565(context.getSystemService()!!.isLowRamDevice) - .allowHardware(true) - .componentRegistry { - if (Build.VERSION.SDK_INT >= 28) { - add(ImageDecoderDecoder(context)) + val imageLoader = ImageLoader.Builder(context).apply { + val callFactoryInit = { Injekt.get().client } + val diskCacheInit = { CoilDiskCache.get(context) } + components { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + add(ImageDecoderDecoder.Factory()) } else { - add(GifDecoder()) + add(GifDecoder.Factory()) } - add(SvgDecoder(context)) - add(MangaFetcher()) - add(ByteArrayFetcher()) + add(TachiyomiImageDecoder.Factory()) + add(MangaCoverFetcher.Factory(lazy(callFactoryInit), lazy(diskCacheInit))) + add(MangaCoverKeyer()) } - .okHttpClient(Injekt.get().coilClient) - .build() - + callFactory(callFactoryInit) + diskCache(diskCacheInit) + memoryCache { MemoryCache.Builder(context).maxSizePercent(0.40).build() } + crossfade(true) + allowRgb565(context.getSystemService()!!.isLowRamDevice) + allowHardware(true) + }.build() Coil.setImageLoader(imageLoader) } } + +/** + * Direct copy of Coil's internal SingletonDiskCache so that [MangaCoverFetcher] can access it. + */ +internal object CoilDiskCache { + + private const val FOLDER_NAME = "image_cache" + private var instance: DiskCache? = null + + @Synchronized + fun get(context: Context): DiskCache { + return instance ?: run { + val safeCacheDir = context.cacheDir.apply { mkdirs() } + // Create the singleton disk cache instance. + DiskCache.Builder() + .directory(safeCacheDir.resolve(FOLDER_NAME)) + .build() + .also { instance = it } + } + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/image/coil/LibraryMangaImageTarget.kt b/app/src/main/java/eu/kanade/tachiyomi/data/image/coil/LibraryMangaImageTarget.kt index 3ded3692d4..40d067dbfa 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/image/coil/LibraryMangaImageTarget.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/image/coil/LibraryMangaImageTarget.kt @@ -34,7 +34,7 @@ class LibraryMangaImageTarget( BitmapFactory.decodeFile(file.path, options) if (options.outWidth == -1 || options.outHeight == -1) { file.delete() - view.context.imageLoader.memoryCache.remove(MemoryCache.Key(manga.key())) + view.context.imageLoader.memoryCache?.remove(MemoryCache.Key(manga.key())) } } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/image/coil/MangaCoverFetcher.kt b/app/src/main/java/eu/kanade/tachiyomi/data/image/coil/MangaCoverFetcher.kt new file mode 100644 index 0000000000..1cf2617b10 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/image/coil/MangaCoverFetcher.kt @@ -0,0 +1,320 @@ +package eu.kanade.tachiyomi.data.image.coil + +import android.webkit.MimeTypeMap +import coil.ImageLoader +import coil.decode.DataSource +import coil.decode.ImageSource +import coil.disk.DiskCache +import coil.fetch.FetchResult +import coil.fetch.Fetcher +import coil.fetch.SourceResult +import coil.network.HttpException +import coil.request.Options +import coil.request.Parameters +import eu.kanade.tachiyomi.data.cache.CoverCache +import eu.kanade.tachiyomi.data.database.models.Manga +import eu.kanade.tachiyomi.network.await +import eu.kanade.tachiyomi.source.SourceManager +import eu.kanade.tachiyomi.source.online.HttpSource +import eu.kanade.tachiyomi.util.manga.MangaCoverMetadata +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.launch +import okhttp3.CacheControl +import okhttp3.Call +import okhttp3.Request +import okhttp3.Response +import okhttp3.internal.closeQuietly +import okio.Path.Companion.toOkioPath +import okio.Source +import okio.buffer +import okio.sink +import okio.source +import timber.log.Timber +import uy.kohesive.injekt.api.get +import uy.kohesive.injekt.injectLazy +import java.io.File +import java.net.HttpURLConnection +import java.util.Date + +class MangaCoverFetcher( + private val manga: Manga, + private val sourceLazy: Lazy, + private val options: Options, + private val coverCache: CoverCache, + private val callFactoryLazy: Lazy, + private val diskCacheLazy: Lazy, +) : Fetcher { + + // For non-custom cover + private val diskCacheKey: String? by lazy { MangaCoverKeyer().key(manga, options) } + private lateinit var url: String + + val fileScope = CoroutineScope(Job() + Dispatchers.IO) + + override suspend fun fetch(): FetchResult { + // diskCacheKey is thumbnail_url + url = manga.thumbnail_url ?: error("No cover specified") + return when (getResourceType(url)) { + Type.URL -> httpLoader() + Type.File -> fileLoader(File(url.substringAfter("file://"))) + null -> error("Invalid image") + } + } + + private suspend fun httpLoader(): FetchResult { + val diskRead = options.diskCachePolicy.readEnabled + val networkRead = options.networkCachePolicy.readEnabled + val onlyCache = !networkRead && diskRead + val shouldFetchRemotely = networkRead && !diskRead && !onlyCache + val useCustomCover = options.parameters.value(useCustomCover) ?: true + // Use custom cover if exists + if (!shouldFetchRemotely) { + val customCoverFile by lazy { coverCache.getCustomCoverFile(manga) } + if (useCustomCover && customCoverFile.exists()) { + setRatioAndColorsInScope(manga, customCoverFile) + return fileLoader(customCoverFile) + } + } + val coverFile = coverCache.getCoverFile(manga) + if (!shouldFetchRemotely && coverFile.exists() && options.diskCachePolicy.readEnabled) { + if (!manga.favorite) { + coverFile.setLastModified(Date().time) + } + setRatioAndColorsInScope(manga, coverFile) + return fileLoader(coverFile) + } + var snapshot = readFromDiskCache() + try { + // Fetch from disk cache + if (snapshot != null) { + val snapshotCoverCache = moveSnapshotToCoverCache(snapshot, coverFile) + if (snapshotCoverCache != null) { + // Read from cover cache after added to library + return fileLoader(snapshotCoverCache) + } + + // Read from snapshot + return SourceResult( + source = snapshot.toImageSource(), + mimeType = "image/*", + dataSource = DataSource.DISK, + ) + } + + // Fetch from network + val response = executeNetworkRequest() + val responseBody = checkNotNull(response.body) { "Null response source" } + try { + // Read from cover cache after library manga cover updated + val responseCoverCache = writeResponseToCoverCache(response, coverFile) + if (responseCoverCache != null) { + return fileLoader(responseCoverCache) + } + + // Read from disk cache + snapshot = writeToDiskCache(snapshot, response) + if (snapshot != null) { + return SourceResult( + source = snapshot.toImageSource(), + mimeType = "image/*", + dataSource = DataSource.NETWORK, + ) + } + + // Read from response if cache is unused or unusable + return SourceResult( + source = ImageSource(source = responseBody.source(), context = options.context), + mimeType = "image/*", + dataSource = if (response.cacheResponse != null) DataSource.DISK else DataSource.NETWORK, + ) + } catch (e: Exception) { + responseBody.closeQuietly() + throw e + } + } catch (e: Exception) { + snapshot?.closeQuietly() + throw e + } + } + + private suspend fun executeNetworkRequest(): Response { + val client = sourceLazy.value?.client ?: callFactoryLazy.value + val response = client.newCall(newRequest()).await() + if (!response.isSuccessful && response.code != HttpURLConnection.HTTP_NOT_MODIFIED) { + response.body?.closeQuietly() + throw HttpException(response) + } + return response + } + + private fun newRequest(): Request { + val request = Request.Builder() + .url(url) + .headers(sourceLazy.value?.headers ?: options.headers) + // Support attaching custom data to the network request. + .tag(Parameters::class.java, options.parameters) + + val diskRead = options.diskCachePolicy.readEnabled + val networkRead = options.networkCachePolicy.readEnabled + val onlyCache = !networkRead && diskRead + val forceNetwork = networkRead && !diskRead + when { + !networkRead && diskRead -> { + request.cacheControl(CacheControl.FORCE_CACHE) + } + networkRead && !diskRead -> if (options.diskCachePolicy.writeEnabled) { + request.cacheControl(CacheControl.FORCE_NETWORK) + } else { + request.cacheControl(CACHE_CONTROL_FORCE_NETWORK_NO_CACHE) + } + !networkRead && !diskRead -> { + // This causes the request to fail with a 504 Unsatisfiable Request. + request.cacheControl(CACHE_CONTROL_NO_NETWORK_NO_CACHE) + } + } + + return request.build() + } + + private fun moveSnapshotToCoverCache(snapshot: DiskCache.Snapshot, cacheFile: File?): File? { + if (cacheFile == null) return null + return try { + diskCacheLazy.value.run { + fileSystem.source(snapshot.data).use { input -> + writeSourceToCoverCache(input, cacheFile) + } + remove(diskCacheKey!!) + } + cacheFile.takeIf { it.exists() } + } catch (e: Exception) { + Timber.e(e, "Failed to write snapshot data to cover cache ${cacheFile.name}") + null + } + } + + private fun writeResponseToCoverCache(response: Response, cacheFile: File?): File? { + if (cacheFile == null || !options.diskCachePolicy.writeEnabled) return null + return try { + response.peekBody(Long.MAX_VALUE).source().use { input -> + writeSourceToCoverCache(input, cacheFile) + } + cacheFile.takeIf { it.exists() } + } catch (e: Exception) { + Timber.e(e, "Failed to write response data to cover cache ${cacheFile.name}") + null + } + } + + private fun writeSourceToCoverCache(input: Source, cacheFile: File) { + cacheFile.parentFile?.mkdirs() + cacheFile.delete() + try { + cacheFile.sink().buffer().use { output -> + output.writeAll(input) + } + } catch (e: Exception) { + cacheFile.delete() + throw e + } + } + + private fun readFromDiskCache(): DiskCache.Snapshot? { + return if (options.diskCachePolicy.readEnabled) diskCacheLazy.value[diskCacheKey!!] else null + } + + private fun writeToDiskCache( + snapshot: DiskCache.Snapshot?, + response: Response, + ): DiskCache.Snapshot? { + if (!options.diskCachePolicy.writeEnabled) { + snapshot?.closeQuietly() + return null + } + val editor = if (snapshot != null) { + snapshot.closeAndEdit() + } else { + diskCacheLazy.value.edit(diskCacheKey!!) + } ?: return null + try { + diskCacheLazy.value.fileSystem.write(editor.data) { + response.body!!.source().readAll(this) + } + return editor.commitAndGet() + } catch (e: Exception) { + try { + editor.abort() + } catch (ignored: Exception) { + } + throw e + } + } + + private fun DiskCache.Snapshot.toImageSource(): ImageSource { + return ImageSource(file = data, diskCacheKey = diskCacheKey, closeable = this) + } + + private fun setRatioAndColorsInScope(manga: Manga, ogFile: File? = null, force: Boolean = false) { + fileScope.launch { + MangaCoverMetadata.setRatioAndColors(manga, ogFile, force) + } + } + + /** Modified from [MimeTypeMap.getFileExtensionFromUrl] to be more permissive with special characters. */ + private fun MimeTypeMap.getMimeTypeFromUrl(url: String?): String? { + if (url.isNullOrBlank()) { + return null + } + + val extension = url + .substringBeforeLast('#') // Strip the fragment. + .substringBeforeLast('?') // Strip the query. + .substringAfterLast('/') // Get the last path segment. + .substringAfterLast('.', missingDelimiterValue = "") // Get the file extension. + + return getMimeTypeFromExtension(extension) + } + + private fun fileLoader(file: File): FetchResult { + return SourceResult( + source = ImageSource(file = file.toOkioPath(), diskCacheKey = diskCacheKey), + 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 + } + } + + class Factory( + private val callFactoryLazy: Lazy, + private val diskCacheLazy: Lazy, + ) : Fetcher.Factory { + + private val coverCache: CoverCache by injectLazy() + private val sourceManager: SourceManager by injectLazy() + + override fun create(data: Manga, options: Options, imageLoader: ImageLoader): Fetcher { + val source = lazy { sourceManager.get(data.source) as? HttpSource } + return MangaCoverFetcher(data, source, options, coverCache, callFactoryLazy, diskCacheLazy) + } + } + + private enum class Type { + File, URL; + } + + companion object { + const val useCustomCover = "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/image/coil/MangaCoverKeyer.kt b/app/src/main/java/eu/kanade/tachiyomi/data/image/coil/MangaCoverKeyer.kt new file mode 100644 index 0000000000..221b3b5a96 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/image/coil/MangaCoverKeyer.kt @@ -0,0 +1,17 @@ +package eu.kanade.tachiyomi.data.image.coil + +import coil.key.Keyer +import coil.request.Options +import eu.kanade.tachiyomi.data.database.models.Manga +import eu.kanade.tachiyomi.util.storage.DiskUtil + +class MangaCoverKeyer : Keyer { + override fun key(data: Manga, options: Options): String? { + if (data.thumbnail_url.isNullOrBlank()) return null + return if (!data.favorite) { + data.thumbnail_url!! + } else { + DiskUtil.hashKeyForDisk(data.thumbnail_url!!) + } + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/image/coil/MangaFetcher.kt b/app/src/main/java/eu/kanade/tachiyomi/data/image/coil/MangaFetcher.kt deleted file mode 100644 index d8ce83f331..0000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/data/image/coil/MangaFetcher.kt +++ /dev/null @@ -1,240 +0,0 @@ -package eu.kanade.tachiyomi.data.image.coil - -import android.graphics.BitmapFactory -import android.webkit.MimeTypeMap -import androidx.palette.graphics.Palette -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 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 eu.kanade.tachiyomi.util.manga.MangaCoverMetadata -import eu.kanade.tachiyomi.util.storage.DiskUtil -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.Job -import kotlinx.coroutines.launch -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 - -class MangaFetcher : Fetcher { - - companion object { - const val realCover = "real_cover" - const val onlyCache = "only_cache" - const val onlyFetchRemotely = "only_fetch_remotely" - } - - private val coverCache: CoverCache by injectLazy() - private val sourceManager: SourceManager by injectLazy() - private val defaultClient = Injekt.get().client - val fileScope = CoroutineScope(Job() + Dispatchers.IO) - - override fun key(data: Manga): String? { - if (data.thumbnail_url.isNullOrBlank()) return null - return if (!data.favorite) { - data.thumbnail_url!! - } else { - DiskUtil.hashKeyForDisk(data.thumbnail_url!!) - } - } - - override suspend fun fetch(pool: BitmapPool, data: Manga, size: Size, options: Options): FetchResult { - 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 onlyCache = options.parameters.value(onlyCache) == true - val shouldFetchRemotely = options.parameters.value(onlyFetchRemotely) == true && !onlyCache - if (!shouldFetchRemotely) { - val customCoverFile = coverCache.getCustomCoverFile(manga) - if (customCoverFile.exists() && options.parameters.value(realCover) != true) { - setRatioAndColorsInScope(manga, customCoverFile) - return fileLoader(customCoverFile) - } - } - val coverFile = coverCache.getCoverFile(manga) - if (!shouldFetchRemotely && coverFile.exists() && options.diskCachePolicy.readEnabled) { - if (!manga.favorite) { - coverFile.setLastModified(Date().time) - } - setRatioAndColorsInScope(manga, coverFile) - return fileLoader(coverFile) - } - val (response, body) = awaitGetCall( - manga, - if (manga.favorite) { - onlyCache - } else { - false - }, - shouldFetchRemotely, - ) - - if (options.diskCachePolicy.writeEnabled) { - val tmpFile = File(coverFile.absolutePath + "_tmp") - body.source().use { input -> - tmpFile.sink().buffer().use { output -> - output.writeAll(input) - } - } - - if (response.isSuccessful || !coverFile.exists()) { - if (coverFile.exists()) { - coverFile.delete() - } - - tmpFile.renameTo(coverFile) - } - if (manga.favorite) { - coverCache.deleteCachedCovers() - } - } - setRatioAndColorsInScope(manga, coverFile, true) - return fileLoader(coverFile) - } - - private fun setRatioAndColorsInScope(manga: Manga, ogFile: File? = null, force: Boolean = false) { - fileScope.launch { - setRatioAndColors(manga, ogFile, force) - } - } - - fun setRatioAndColors(manga: Manga, ogFile: File? = null, force: Boolean = false) { - if (!manga.favorite) { - MangaCoverMetadata.remove(manga) - } - if (manga.vibrantCoverColor != null && !manga.favorite) return - val file = ogFile ?: coverCache.getCustomCoverFile(manga).takeIf { it.exists() } ?: coverCache.getCoverFile(manga) - // if the file exists and the there was still an error then the file is corrupted - if (file.exists()) { - val options = BitmapFactory.Options() - val hasVibrantColor = if (manga.favorite) manga.vibrantCoverColor != null else true - if (manga.dominantCoverColors != null && hasVibrantColor && !force) { - options.inJustDecodeBounds = true - } else { - options.inSampleSize = 4 - } - val bitmap = BitmapFactory.decodeFile(file.path, options) ?: return - if (!options.inJustDecodeBounds) { - Palette.from(bitmap).generate { - if (it == null) return@generate - if (manga.favorite) { - it.dominantSwatch?.let { swatch -> - manga.dominantCoverColors = swatch.rgb to swatch.titleTextColor - } - } - val color = it.getBestColor() ?: return@generate - manga.vibrantCoverColor = color - } - } - if (manga.favorite && !(options.outWidth == -1 || options.outHeight == -1)) { - MangaCoverMetadata.addCoverRatio(manga, options.outWidth / options.outHeight.toFloat()) - } - } - } - - private suspend fun awaitGetCall(manga: Manga, onlyCache: Boolean = false, forceNetwork: Boolean): Pair { - val call = getCall(manga, onlyCache, forceNetwork) - val response = call.await() - return response to checkNotNull(response.body) { "Null response source" } - } - - private fun getCall(manga: Manga, onlyCache: Boolean, forceNetwork: Boolean): 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) - } - if (forceNetwork) { - it.cacheControl(CacheControl.FORCE_NETWORK) - } else if (onlyCache) { - it.cacheControl(CacheControl.FORCE_CACHE) - } - }.build() - - return newClient.newCall(request) - } - - /** - * "text/plain" is often used as a default/fallback MIME type. - * Attempt to guess a better MIME type from the file extension. - */ - private fun getMimeType(data: String, body: ResponseBody): String? { - val rawContentType = body.contentType()?.toString() - return if (rawContentType == null || rawContentType.startsWith("text/plain")) { - MimeTypeMap.getSingleton().getMimeTypeFromUrl(data) ?: rawContentType - } else { - rawContentType - } - } - - /** Modified from [MimeTypeMap.getFileExtensionFromUrl] to be more permissive with special characters. */ - private fun MimeTypeMap.getMimeTypeFromUrl(url: String?): String? { - if (url.isNullOrBlank()) { - return null - } - - val extension = url - .substringBeforeLast('#') // Strip the fragment. - .substringBeforeLast('?') // Strip the query. - .substringAfterLast('/') // Get the last path segment. - .substringAfterLast('.', missingDelimiterValue = "") // Get the file extension. - - return getMimeTypeFromExtension(extension) - } - - 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; - } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/image/coil/TachiyomiImageDecoder.kt b/app/src/main/java/eu/kanade/tachiyomi/data/image/coil/TachiyomiImageDecoder.kt new file mode 100644 index 0000000000..36a46c253b --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/image/coil/TachiyomiImageDecoder.kt @@ -0,0 +1,61 @@ +package eu.kanade.tachiyomi.data.image.coil + +import android.os.Build +import androidx.core.graphics.drawable.toDrawable +import coil.ImageLoader +import coil.decode.DecodeResult +import coil.decode.Decoder +import coil.decode.ImageDecoderDecoder +import coil.decode.ImageSource +import coil.fetch.SourceResult +import coil.request.Options +import eu.kanade.tachiyomi.util.system.ImageUtil +import okio.BufferedSource +import tachiyomi.decoder.ImageDecoder + +/** + * A [Decoder] that uses built-in [ImageDecoder] to decode images that is not supported by the system. + */ +class TachiyomiImageDecoder(private val resources: ImageSource, private val options: Options) : Decoder { + + override suspend fun decode(): DecodeResult { + val decoder = resources.sourceOrNull()?.use { + ImageDecoder.newInstance(it.inputStream()) + } + + check(decoder != null && decoder.width > 0 && decoder.height > 0) { "Failed to initialize decoder." } + + val bitmap = decoder.decode(rgb565 = options.allowRgb565) + decoder.recycle() + + check(bitmap != null) { "Failed to decode image." } + + return DecodeResult( + drawable = bitmap.toDrawable(options.context.resources), + isSampled = false, + ) + } + + class Factory : Decoder.Factory { + + override fun create(result: SourceResult, options: Options, imageLoader: ImageLoader): Decoder? { + if (!isApplicable(result.source.source())) return null + return TachiyomiImageDecoder(result.source, options) + } + + private fun isApplicable(source: BufferedSource): Boolean { + val type = source.peek().inputStream().use { + ImageUtil.findImageType(it) + } + return when (type) { + ImageUtil.ImageType.AVIF/*, ImageUtil.ImageType.JXL */ -> true + ImageUtil.ImageType.HEIF -> Build.VERSION.SDK_INT < Build.VERSION_CODES.O + else -> false + } + } + + override fun equals(other: Any?) = other is ImageDecoderDecoder.Factory + + override fun hashCode() = javaClass.hashCode() + } +} 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 a6dcdaf162..04dbf071f3 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 @@ -13,13 +13,11 @@ import androidx.core.content.ContextCompat import coil.Coil import coil.request.CachePolicy import coil.request.ImageRequest -import coil.request.Parameters import coil.transform.CircleCropTransformation import eu.kanade.tachiyomi.R 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.image.coil.MangaFetcher import eu.kanade.tachiyomi.data.notification.NotificationHandler import eu.kanade.tachiyomi.data.notification.NotificationReceiver import eu.kanade.tachiyomi.data.notification.Notifications @@ -177,11 +175,8 @@ class LibraryUpdateNotifier(private val context: Context) { setSmallIcon(R.drawable.ic_tachij2k_notification) try { val request = ImageRequest.Builder(context).data(manga) - .parameters( - Parameters.Builder().set(MangaFetcher.onlyCache, true) - .build(), - ) - .networkCachePolicy(CachePolicy.READ_ONLY) + .networkCachePolicy(CachePolicy.DISABLED) + .diskCachePolicy(CachePolicy.ENABLED) .transformations(CircleCropTransformation()) .size(width = ICON_SIZE, height = ICON_SIZE).build() 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 7bf3091b38..ea0ffff9af 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 @@ -10,7 +10,6 @@ import androidx.work.NetworkType import coil.Coil import coil.request.CachePolicy import coil.request.ImageRequest -import coil.request.Parameters import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.cache.CoverCache import eu.kanade.tachiyomi.data.database.DatabaseHelper @@ -21,7 +20,6 @@ import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.toMangaInfo import eu.kanade.tachiyomi.data.download.DownloadManager import eu.kanade.tachiyomi.data.download.DownloadService -import eu.kanade.tachiyomi.data.image.coil.MangaFetcher import eu.kanade.tachiyomi.data.library.LibraryUpdateService.Companion.start import eu.kanade.tachiyomi.data.notification.Notifications import eu.kanade.tachiyomi.data.preference.MANGA_HAS_UNREAD @@ -505,7 +503,7 @@ class LibraryUpdateService( val request = ImageRequest.Builder(this@LibraryUpdateService).data(manga) .memoryCachePolicy(CachePolicy.DISABLED) - .parameters(Parameters.Builder().set(MangaFetcher.onlyFetchRemotely, true).build()) + .diskCachePolicy(CachePolicy.WRITE_ONLY) .build() Coil.imageLoader(this@LibraryUpdateService).execute(request) } 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 9185cc4332..91ccfdcfb0 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/network/NetworkHelper.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/network/NetworkHelper.kt @@ -1,7 +1,6 @@ package eu.kanade.tachiyomi.network import android.content.Context -import coil.util.CoilUtils import com.chuckerteam.chucker.api.ChuckerCollector import com.chuckerteam.chucker.api.ChuckerInterceptor import eu.kanade.tachiyomi.BuildConfig @@ -57,8 +56,6 @@ class NetworkHelper(val context: Context) { val client by lazy { baseClientBuilder.cache(Cache(cacheDir, cacheSize)).build() } - val coilClient by lazy { baseClientBuilder.cache(CoilUtils.createDefaultCache(context)).build() } - val cloudflareClient by lazy { client.newBuilder() .addInterceptor(CloudflareInterceptor(context)) 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 1f1fee9cc7..3fff725df4 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 @@ -9,7 +9,7 @@ import androidx.core.text.color import androidx.core.text.scale import androidx.core.view.isGone import androidx.core.view.isVisible -import coil.clear +import coil.dispose import coil.load import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.image.coil.CoverViewTarget @@ -99,7 +99,7 @@ class ExtensionHolder(view: View, val adapter: ExtensionAdapter) : binding.installProgress.isVisible = item.sessionProgress != null binding.cancelButton.isVisible = item.sessionProgress != null - binding.sourceImage.clear() + binding.sourceImage.dispose() if (extension is Extension.Available) { binding.sourceImage.load(extension.iconUrl) { 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 22e9f9c34e..aefabc573c 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 @@ -10,7 +10,7 @@ import androidx.constraintlayout.widget.ConstraintLayout import androidx.core.view.isVisible import androidx.core.view.marginBottom import androidx.core.view.updateLayoutParams -import coil.clear +import coil.dispose import coil.size.Precision import coil.size.Scale import eu.kanade.tachiyomi.R @@ -99,7 +99,7 @@ class LibraryGridHolder( setSelected(adapter.isSelected(flexibleAdapterPosition)) // Update the cover. - binding.coverThumbnail.clear() + binding.coverThumbnail.dispose() setCover(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 ba3296ff13..18a9a6d68f 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 @@ -4,7 +4,7 @@ import android.view.View import android.view.ViewGroup import androidx.core.view.isVisible import androidx.core.view.updateLayoutParams -import coil.clear +import coil.dispose import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.image.coil.loadManga import eu.kanade.tachiyomi.databinding.MangaListItemBinding @@ -90,7 +90,7 @@ class LibraryListHolder( } // Update the cover. - binding.coverThumbnail.clear() + binding.coverThumbnail.dispose() binding.coverThumbnail.loadManga(item.manga) } 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 7207460527..9f83337ec0 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 @@ -10,7 +10,6 @@ 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.download.DownloadManager -import eu.kanade.tachiyomi.data.image.coil.MangaFetcher import eu.kanade.tachiyomi.data.preference.DelayedLibrarySuggestionsJob import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.minusAssign @@ -1222,10 +1221,9 @@ class LibraryPresenter( suspend fun updateRatiosAndColors() { val db: DatabaseHelper = Injekt.get() - val mangaFetcher = MangaFetcher() val libraryManga = db.getFavoriteMangas().executeOnIO() libraryManga.forEach { manga -> - try { withUIContext { mangaFetcher.setRatioAndColors(manga) } } catch (_: Exception) { } + try { withUIContext { MangaCoverMetadata.setRatioAndColors(manga) } } catch (_: Exception) { } } MangaCoverMetadata.savePrefs() } 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 66f9556593..8ab52a60f9 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 @@ -13,14 +13,14 @@ import android.view.inputmethod.InputMethodManager import androidx.core.graphics.ColorUtils import androidx.core.view.children import androidx.core.view.isVisible -import coil.loadAny +import coil.load import coil.request.Parameters import com.google.android.material.chip.Chip import com.google.android.material.chip.ChipGroup 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.image.coil.MangaFetcher +import eu.kanade.tachiyomi.data.image.coil.MangaCoverFetcher import eu.kanade.tachiyomi.data.image.coil.loadManga import eu.kanade.tachiyomi.databinding.EditMangaDialogBinding import eu.kanade.tachiyomi.source.LocalSource @@ -176,10 +176,10 @@ class EditMangaDialog : DialogController { binding.resetCover.isVisible = !isLocal binding.resetCover.setOnClickListener { - binding.mangaCover.loadAny( + binding.mangaCover.load( manga, builder = { - parameters(Parameters.Builder().set(MangaFetcher.realCover, true).build()) + parameters(Parameters.Builder().set(MangaCoverFetcher.useCustomCover, false).build()) }, ) customCoverUri = null @@ -288,7 +288,7 @@ class EditMangaDialog : DialogController { fun updateCover(uri: Uri) { willResetCover = false - binding.mangaCover.loadAny(uri) + binding.mangaCover.load(uri) customCoverUri = uri } 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 8a04d0a668..d347361a7c 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 @@ -9,7 +9,6 @@ import coil.imageLoader import coil.memory.MemoryCache import coil.request.CachePolicy import coil.request.ImageRequest -import coil.request.Parameters import coil.request.SuccessResult import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.cache.CoverCache @@ -22,7 +21,6 @@ import eu.kanade.tachiyomi.data.database.models.toMangaInfo import eu.kanade.tachiyomi.data.download.DownloadManager import eu.kanade.tachiyomi.data.download.model.Download import eu.kanade.tachiyomi.data.download.model.DownloadQueue -import eu.kanade.tachiyomi.data.image.coil.MangaFetcher import eu.kanade.tachiyomi.data.library.CustomMangaManager import eu.kanade.tachiyomi.data.library.LibraryServiceListener import eu.kanade.tachiyomi.data.library.LibraryUpdateService @@ -354,14 +352,11 @@ class MangaDetailsPresenter( val request = ImageRequest.Builder(preferences.context).data(manga) .memoryCachePolicy(CachePolicy.DISABLED) - .parameters( - Parameters.Builder().set(MangaFetcher.onlyFetchRemotely, true) - .build(), - ) + .diskCachePolicy(CachePolicy.WRITE_ONLY) .build() if (Coil.imageLoader(preferences.context).execute(request) is SuccessResult) { - preferences.context.imageLoader.memoryCache.remove(MemoryCache.Key(manga.key())) + preferences.context.imageLoader.memoryCache?.remove(MemoryCache.Key(manga.key())) withContext(Dispatchers.Main) { controller?.setPaletteColor() } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackSearchItem.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackSearchItem.kt index e1bc4e9bfb..44f6e2d05e 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackSearchItem.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackSearchItem.kt @@ -2,7 +2,7 @@ package eu.kanade.tachiyomi.ui.manga.track import android.view.View import androidx.core.view.isVisible -import coil.clear +import coil.dispose import coil.load import com.google.android.material.shape.CornerFamily import com.mikepenz.fastadapter.FastAdapter @@ -45,7 +45,7 @@ class TrackSearchItem(val trackSearch: TrackSearch) : AbstractItem it.recycle() - is AppCompatImageView -> it.clear() + is AppCompatImageView -> it.dispose() } it.isVisible = false } 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 f33a03b1fa..e0e2fec159 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 @@ -23,7 +23,7 @@ import android.widget.LinearLayout import android.widget.TextView import androidx.core.net.toUri import androidx.core.view.isVisible -import coil.loadAny +import coil.load import coil.request.CachePolicy import com.davemorrissey.labs.subscaleview.ImageSource import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView @@ -954,7 +954,7 @@ class PagerPageHolder( * Extension method to set a [stream] into this ImageView. */ private fun ImageView.setImage(stream: InputStream) { - this.loadAny(stream.readBytes()) { + this.load(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/source/browse/BrowseSourceGridHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/source/browse/BrowseSourceGridHolder.kt index b534bff679..94727fae34 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 @@ -5,12 +5,13 @@ import android.view.View import androidx.core.view.isVisible import androidx.recyclerview.widget.RecyclerView import coil.Coil -import coil.clear +import coil.dispose import coil.request.ImageRequest import eu.davidea.flexibleadapter.FlexibleAdapter import eu.davidea.flexibleadapter.items.IFlexible import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.image.coil.CoverViewTarget +import eu.kanade.tachiyomi.data.image.coil.MangaCoverFetcher import eu.kanade.tachiyomi.databinding.MangaGridItemBinding import eu.kanade.tachiyomi.ui.library.LibraryCategoryAdapter import eu.kanade.tachiyomi.util.view.setCards @@ -61,11 +62,13 @@ class BrowseSourceGridHolder( override fun setImage(manga: Manga) { if ((view.context as? Activity)?.isDestroyed == true) return if (manga.thumbnail_url == null) { - binding.coverThumbnail.clear() + binding.coverThumbnail.dispose() } else { manga.id ?: return val request = ImageRequest.Builder(view.context).data(manga) - .target(CoverViewTarget(binding.coverThumbnail, binding.progress)).build() + .target(CoverViewTarget(binding.coverThumbnail, binding.progress)) + .setParameter(MangaCoverFetcher.useCustomCover, false) + .build() Coil.imageLoader(view.context).enqueue(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 a5848cbeaf..ddc16d9fbf 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 @@ -4,12 +4,13 @@ import android.view.View import androidx.core.view.isVisible import androidx.recyclerview.widget.RecyclerView import coil.Coil -import coil.clear +import coil.dispose import coil.request.ImageRequest import eu.davidea.flexibleadapter.FlexibleAdapter import eu.davidea.flexibleadapter.items.IFlexible import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.image.coil.CoverViewTarget +import eu.kanade.tachiyomi.data.image.coil.MangaCoverFetcher import eu.kanade.tachiyomi.databinding.MangaListItemBinding import eu.kanade.tachiyomi.util.view.setCards @@ -50,11 +51,13 @@ class BrowseSourceListHolder( override fun setImage(manga: Manga) { // Update the cover. if (manga.thumbnail_url == null) { - binding.coverThumbnail.clear() + binding.coverThumbnail.dispose() } else { manga.id ?: return val request = ImageRequest.Builder(view.context).data(manga) - .target(CoverViewTarget(binding.coverThumbnail)).build() + .target(CoverViewTarget(binding.coverThumbnail)) + .setParameter(MangaCoverFetcher.useCustomCover, false) + .build() Coil.imageLoader(view.context).enqueue(request) } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/source/globalsearch/GlobalSearchMangaHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/source/globalsearch/GlobalSearchMangaHolder.kt index 558879df00..b1e2135119 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/source/globalsearch/GlobalSearchMangaHolder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/source/globalsearch/GlobalSearchMangaHolder.kt @@ -4,11 +4,12 @@ import android.graphics.drawable.RippleDrawable import android.view.View import androidx.core.view.isVisible import coil.Coil -import coil.clear +import coil.dispose import coil.request.CachePolicy import coil.request.ImageRequest import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.image.coil.CoverViewTarget +import eu.kanade.tachiyomi.data.image.coil.MangaCoverFetcher import eu.kanade.tachiyomi.databinding.SourceGlobalSearchControllerCardItemBinding import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder import eu.kanade.tachiyomi.util.system.dpToPx @@ -50,12 +51,14 @@ class GlobalSearchMangaHolder(view: View, adapter: GlobalSearchCardAdapter) : } fun setImage(manga: Manga) { - binding.itemImage.clear() + binding.itemImage.dispose() if (!manga.thumbnail_url.isNullOrEmpty()) { val request = ImageRequest.Builder(itemView.context).data(manga) .placeholder(android.R.color.transparent) .memoryCachePolicy(CachePolicy.DISABLED) - .target(CoverViewTarget(binding.itemImage, binding.progress)).build() + .target(CoverViewTarget(binding.itemImage, binding.progress)) + .setParameter(MangaCoverFetcher.useCustomCover, false) + .build() Coil.imageLoader(itemView.context).enqueue(request) } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/manga/MangaCoverMetadata.kt b/app/src/main/java/eu/kanade/tachiyomi/util/manga/MangaCoverMetadata.kt index 86cc604d64..18b5cc430c 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/util/manga/MangaCoverMetadata.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/util/manga/MangaCoverMetadata.kt @@ -1,9 +1,14 @@ package eu.kanade.tachiyomi.util.manga +import android.graphics.BitmapFactory import androidx.annotation.ColorInt +import androidx.palette.graphics.Palette +import eu.kanade.tachiyomi.data.cache.CoverCache import eu.kanade.tachiyomi.data.database.models.Manga +import eu.kanade.tachiyomi.data.image.coil.getBestColor import eu.kanade.tachiyomi.data.preference.PreferencesHelper import uy.kohesive.injekt.injectLazy +import java.io.File import java.util.concurrent.ConcurrentHashMap /** Object that holds info about a covers size ratio + dominant colors */ @@ -11,6 +16,7 @@ object MangaCoverMetadata { private var coverRatioMap = ConcurrentHashMap() private var coverColorMap = ConcurrentHashMap>() val preferences by injectLazy() + val coverCache by injectLazy() fun load() { val ratios = preferences.coverRatios().get() @@ -42,6 +48,40 @@ object MangaCoverMetadata { ) } + fun setRatioAndColors(manga: Manga, ogFile: File? = null, force: Boolean = false) { + if (!manga.favorite) { + MangaCoverMetadata.remove(manga) + } + if (manga.vibrantCoverColor != null && !manga.favorite) return + val file = ogFile ?: coverCache.getCustomCoverFile(manga).takeIf { it.exists() } ?: coverCache.getCoverFile(manga) + // if the file exists and the there was still an error then the file is corrupted + if (file.exists()) { + val options = BitmapFactory.Options() + val hasVibrantColor = if (manga.favorite) manga.vibrantCoverColor != null else true + if (manga.dominantCoverColors != null && hasVibrantColor && !force) { + options.inJustDecodeBounds = true + } else { + options.inSampleSize = 4 + } + val bitmap = BitmapFactory.decodeFile(file.path, options) ?: return + if (!options.inJustDecodeBounds) { + Palette.from(bitmap).generate { + if (it == null) return@generate + if (manga.favorite) { + it.dominantSwatch?.let { swatch -> + manga.dominantCoverColors = swatch.rgb to swatch.titleTextColor + } + } + val color = it.getBestColor() ?: return@generate + manga.vibrantCoverColor = color + } + } + if (manga.favorite && !(options.outWidth == -1 || options.outHeight == -1)) { + addCoverRatio(manga, options.outWidth / options.outHeight.toFloat()) + } + } + } + fun remove(manga: Manga) { val id = manga.id ?: return coverRatioMap.remove(id)