Coil 2.x upgrade

Co-Authored-By: Ivan Iskandar <12537387+ivaniskandar@users.noreply.github.com>
This commit is contained in:
Jays2Kings 2022-05-02 17:07:06 -04:00
parent cd1e3e1f11
commit bc778347fd
27 changed files with 533 additions and 346 deletions

View file

@ -200,7 +200,7 @@ dependencies {
implementation("com.github.inorichi.injekt:injekt-core:65b0440") implementation("com.github.inorichi.injekt:injekt-core:65b0440")
// Image library // Image library
val coilVersion = "1.3.2" val coilVersion = "2.0.0-rc03"
implementation("io.coil-kt:coil:$coilVersion") implementation("io.coil-kt:coil:$coilVersion")
implementation("io.coil-kt:coil-gif:$coilVersion") implementation("io.coil-kt:coil-gif:$coilVersion")
implementation("io.coil-kt:coil-svg:$coilVersion") implementation("io.coil-kt:coil-svg:$coilVersion")
@ -270,6 +270,7 @@ tasks {
"-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi", "-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi",
"-opt-in=kotlinx.coroutines.InternalCoroutinesApi", "-opt-in=kotlinx.coroutines.InternalCoroutinesApi",
"-opt-in=kotlinx.serialization.ExperimentalSerializationApi", "-opt-in=kotlinx.serialization.ExperimentalSerializationApi",
"-opt-in=coil.annotation.ExperimentalCoilApi",
) )
} }

View file

@ -112,7 +112,7 @@ class CoverCache(val context: Context) {
), ),
) )
} }
context.imageLoader.memoryCache.clear() context.imageLoader.memoryCache?.clear()
lastClean = System.currentTimeMillis() lastClean = System.currentTimeMillis()
} }
@ -169,7 +169,7 @@ class CoverCache(val context: Context) {
fun setCustomCoverToCache(manga: Manga, inputStream: InputStream) { fun setCustomCoverToCache(manga: Manga, inputStream: InputStream) {
getCustomCoverFile(manga).outputStream().use { getCustomCoverFile(manga).outputStream().use {
inputStream.copyTo(it) 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 { val result = getCustomCoverFile(manga).let {
it.exists() && it.delete() it.exists() && it.delete()
} }
context.imageLoader.memoryCache.remove(MemoryCache.Key(manga.key())) context.imageLoader.memoryCache?.remove(MemoryCache.Key(manga.key()))
return result return result
} }
@ -205,7 +205,7 @@ class CoverCache(val context: Context) {
fun deleteFromCache(name: String?) { fun deleteFromCache(name: String?) {
if (name.isNullOrEmpty()) return if (name.isNullOrEmpty()) return
val file = getCoverFile(MangaImpl().apply { thumbnail_url = name }) 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() if (file.exists()) file.delete()
} }
@ -226,7 +226,7 @@ class CoverCache(val context: Context) {
val file = getCoverFile(manga) val file = getCoverFile(manga)
if (deleteCustom) deleteCustomCover(manga) if (deleteCustom) deleteCustomCover(manga)
if (file.exists()) { if (file.exists()) {
context.imageLoader.memoryCache.remove(MemoryCache.Key(manga.key())) context.imageLoader.memoryCache?.remove(MemoryCache.Key(manga.key()))
file.delete() file.delete()
} }
} }

View file

@ -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<ByteArray> {
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,
)
}
}

View file

@ -3,37 +3,60 @@ package eu.kanade.tachiyomi.data.image.coil
import android.app.ActivityManager import android.app.ActivityManager
import android.content.Context import android.content.Context
import android.os.Build import android.os.Build
import androidx.core.content.ContextCompat.getSystemService
import androidx.core.content.getSystemService import androidx.core.content.getSystemService
import coil.Coil import coil.Coil
import coil.ImageLoader import coil.ImageLoader
import coil.decode.GifDecoder import coil.decode.GifDecoder
import coil.decode.ImageDecoderDecoder import coil.decode.ImageDecoderDecoder
import coil.decode.SvgDecoder import coil.disk.DiskCache
import coil.memory.MemoryCache
import eu.kanade.tachiyomi.network.NetworkHelper import eu.kanade.tachiyomi.network.NetworkHelper
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
class CoilSetup(context: Context) { class CoilSetup(context: Context) {
init { init {
val imageLoader = ImageLoader.Builder(context) val imageLoader = ImageLoader.Builder(context).apply {
.availableMemoryPercentage(0.40) val callFactoryInit = { Injekt.get<NetworkHelper>().client }
.crossfade(true) val diskCacheInit = { CoilDiskCache.get(context) }
.allowRgb565(context.getSystemService<ActivityManager>()!!.isLowRamDevice) components {
.allowHardware(true) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
.componentRegistry { add(ImageDecoderDecoder.Factory())
if (Build.VERSION.SDK_INT >= 28) {
add(ImageDecoderDecoder(context))
} else { } else {
add(GifDecoder()) add(GifDecoder.Factory())
} }
add(SvgDecoder(context)) add(TachiyomiImageDecoder.Factory())
add(MangaFetcher()) add(MangaCoverFetcher.Factory(lazy(callFactoryInit), lazy(diskCacheInit)))
add(ByteArrayFetcher()) add(MangaCoverKeyer())
} }
.okHttpClient(Injekt.get<NetworkHelper>().coilClient) callFactory(callFactoryInit)
.build() diskCache(diskCacheInit)
memoryCache { MemoryCache.Builder(context).maxSizePercent(0.40).build() }
crossfade(true)
allowRgb565(context.getSystemService<ActivityManager>()!!.isLowRamDevice)
allowHardware(true)
}.build()
Coil.setImageLoader(imageLoader) 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 }
}
}
}

View file

@ -34,7 +34,7 @@ class LibraryMangaImageTarget(
BitmapFactory.decodeFile(file.path, options) BitmapFactory.decodeFile(file.path, options)
if (options.outWidth == -1 || options.outHeight == -1) { if (options.outWidth == -1 || options.outHeight == -1) {
file.delete() file.delete()
view.context.imageLoader.memoryCache.remove(MemoryCache.Key(manga.key())) view.context.imageLoader.memoryCache?.remove(MemoryCache.Key(manga.key()))
} }
} }
} }

View file

@ -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<HttpSource?>,
private val options: Options,
private val coverCache: CoverCache,
private val callFactoryLazy: Lazy<Call.Factory>,
private val diskCacheLazy: Lazy<DiskCache>,
) : 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<Call.Factory>,
private val diskCacheLazy: Lazy<DiskCache>,
) : Fetcher.Factory<Manga> {
private val coverCache: CoverCache by injectLazy()
private val sourceManager: SourceManager by injectLazy()
override fun create(data: Manga, options: Options, imageLoader: ImageLoader): Fetcher {
val source = lazy { sourceManager.get(data.source) as? HttpSource }
return MangaCoverFetcher(data, source, options, coverCache, callFactoryLazy, diskCacheLazy)
}
}
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()
}
}

View file

@ -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<Manga> {
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!!)
}
}
}

View file

@ -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<Manga> {
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<NetworkHelper>().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<Response,
ResponseBody,> {
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;
}
}

View file

@ -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()
}
}

View file

@ -13,13 +13,11 @@ import androidx.core.content.ContextCompat
import coil.Coil import coil.Coil
import coil.request.CachePolicy import coil.request.CachePolicy
import coil.request.ImageRequest import coil.request.ImageRequest
import coil.request.Parameters
import coil.transform.CircleCropTransformation import coil.transform.CircleCropTransformation
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.Chapter import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.data.database.models.LibraryManga import eu.kanade.tachiyomi.data.database.models.LibraryManga
import eu.kanade.tachiyomi.data.database.models.Manga 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.NotificationHandler
import eu.kanade.tachiyomi.data.notification.NotificationReceiver import eu.kanade.tachiyomi.data.notification.NotificationReceiver
import eu.kanade.tachiyomi.data.notification.Notifications import eu.kanade.tachiyomi.data.notification.Notifications
@ -177,11 +175,8 @@ class LibraryUpdateNotifier(private val context: Context) {
setSmallIcon(R.drawable.ic_tachij2k_notification) setSmallIcon(R.drawable.ic_tachij2k_notification)
try { try {
val request = ImageRequest.Builder(context).data(manga) val request = ImageRequest.Builder(context).data(manga)
.parameters( .networkCachePolicy(CachePolicy.DISABLED)
Parameters.Builder().set(MangaFetcher.onlyCache, true) .diskCachePolicy(CachePolicy.ENABLED)
.build(),
)
.networkCachePolicy(CachePolicy.READ_ONLY)
.transformations(CircleCropTransformation()) .transformations(CircleCropTransformation())
.size(width = ICON_SIZE, height = ICON_SIZE).build() .size(width = ICON_SIZE, height = ICON_SIZE).build()

View file

@ -10,7 +10,6 @@ import androidx.work.NetworkType
import coil.Coil import coil.Coil
import coil.request.CachePolicy import coil.request.CachePolicy
import coil.request.ImageRequest import coil.request.ImageRequest
import coil.request.Parameters
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.cache.CoverCache import eu.kanade.tachiyomi.data.cache.CoverCache
import eu.kanade.tachiyomi.data.database.DatabaseHelper 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.database.models.toMangaInfo
import eu.kanade.tachiyomi.data.download.DownloadManager import eu.kanade.tachiyomi.data.download.DownloadManager
import eu.kanade.tachiyomi.data.download.DownloadService 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.library.LibraryUpdateService.Companion.start
import eu.kanade.tachiyomi.data.notification.Notifications import eu.kanade.tachiyomi.data.notification.Notifications
import eu.kanade.tachiyomi.data.preference.MANGA_HAS_UNREAD import eu.kanade.tachiyomi.data.preference.MANGA_HAS_UNREAD
@ -505,7 +503,7 @@ class LibraryUpdateService(
val request = val request =
ImageRequest.Builder(this@LibraryUpdateService).data(manga) ImageRequest.Builder(this@LibraryUpdateService).data(manga)
.memoryCachePolicy(CachePolicy.DISABLED) .memoryCachePolicy(CachePolicy.DISABLED)
.parameters(Parameters.Builder().set(MangaFetcher.onlyFetchRemotely, true).build()) .diskCachePolicy(CachePolicy.WRITE_ONLY)
.build() .build()
Coil.imageLoader(this@LibraryUpdateService).execute(request) Coil.imageLoader(this@LibraryUpdateService).execute(request)
} }

View file

@ -1,7 +1,6 @@
package eu.kanade.tachiyomi.network package eu.kanade.tachiyomi.network
import android.content.Context import android.content.Context
import coil.util.CoilUtils
import com.chuckerteam.chucker.api.ChuckerCollector import com.chuckerteam.chucker.api.ChuckerCollector
import com.chuckerteam.chucker.api.ChuckerInterceptor import com.chuckerteam.chucker.api.ChuckerInterceptor
import eu.kanade.tachiyomi.BuildConfig 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 client by lazy { baseClientBuilder.cache(Cache(cacheDir, cacheSize)).build() }
val coilClient by lazy { baseClientBuilder.cache(CoilUtils.createDefaultCache(context)).build() }
val cloudflareClient by lazy { val cloudflareClient by lazy {
client.newBuilder() client.newBuilder()
.addInterceptor(CloudflareInterceptor(context)) .addInterceptor(CloudflareInterceptor(context))

View file

@ -9,7 +9,7 @@ import androidx.core.text.color
import androidx.core.text.scale import androidx.core.text.scale
import androidx.core.view.isGone import androidx.core.view.isGone
import androidx.core.view.isVisible import androidx.core.view.isVisible
import coil.clear import coil.dispose
import coil.load import coil.load
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.image.coil.CoverViewTarget 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.installProgress.isVisible = item.sessionProgress != null
binding.cancelButton.isVisible = item.sessionProgress != null binding.cancelButton.isVisible = item.sessionProgress != null
binding.sourceImage.clear() binding.sourceImage.dispose()
if (extension is Extension.Available) { if (extension is Extension.Available) {
binding.sourceImage.load(extension.iconUrl) { binding.sourceImage.load(extension.iconUrl) {

View file

@ -10,7 +10,7 @@ import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.core.view.marginBottom import androidx.core.view.marginBottom
import androidx.core.view.updateLayoutParams import androidx.core.view.updateLayoutParams
import coil.clear import coil.dispose
import coil.size.Precision import coil.size.Precision
import coil.size.Scale import coil.size.Scale
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
@ -99,7 +99,7 @@ class LibraryGridHolder(
setSelected(adapter.isSelected(flexibleAdapterPosition)) setSelected(adapter.isSelected(flexibleAdapterPosition))
// Update the cover. // Update the cover.
binding.coverThumbnail.clear() binding.coverThumbnail.dispose()
setCover(item.manga) setCover(item.manga)
} }

View file

@ -4,7 +4,7 @@ import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.core.view.updateLayoutParams import androidx.core.view.updateLayoutParams
import coil.clear import coil.dispose
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.image.coil.loadManga import eu.kanade.tachiyomi.data.image.coil.loadManga
import eu.kanade.tachiyomi.databinding.MangaListItemBinding import eu.kanade.tachiyomi.databinding.MangaListItemBinding
@ -90,7 +90,7 @@ class LibraryListHolder(
} }
// Update the cover. // Update the cover.
binding.coverThumbnail.clear() binding.coverThumbnail.dispose()
binding.coverThumbnail.loadManga(item.manga) binding.coverThumbnail.loadManga(item.manga)
} }

View file

@ -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.Manga
import eu.kanade.tachiyomi.data.database.models.MangaCategory import eu.kanade.tachiyomi.data.database.models.MangaCategory
import eu.kanade.tachiyomi.data.download.DownloadManager 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.DelayedLibrarySuggestionsJob
import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.preference.minusAssign import eu.kanade.tachiyomi.data.preference.minusAssign
@ -1222,10 +1221,9 @@ class LibraryPresenter(
suspend fun updateRatiosAndColors() { suspend fun updateRatiosAndColors() {
val db: DatabaseHelper = Injekt.get() val db: DatabaseHelper = Injekt.get()
val mangaFetcher = MangaFetcher()
val libraryManga = db.getFavoriteMangas().executeOnIO() val libraryManga = db.getFavoriteMangas().executeOnIO()
libraryManga.forEach { manga -> libraryManga.forEach { manga ->
try { withUIContext { mangaFetcher.setRatioAndColors(manga) } } catch (_: Exception) { } try { withUIContext { MangaCoverMetadata.setRatioAndColors(manga) } } catch (_: Exception) { }
} }
MangaCoverMetadata.savePrefs() MangaCoverMetadata.savePrefs()
} }

View file

@ -13,14 +13,14 @@ import android.view.inputmethod.InputMethodManager
import androidx.core.graphics.ColorUtils import androidx.core.graphics.ColorUtils
import androidx.core.view.children import androidx.core.view.children
import androidx.core.view.isVisible import androidx.core.view.isVisible
import coil.loadAny import coil.load
import coil.request.Parameters import coil.request.Parameters
import com.google.android.material.chip.Chip import com.google.android.material.chip.Chip
import com.google.android.material.chip.ChipGroup import com.google.android.material.chip.ChipGroup
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.DatabaseHelper import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.data.database.models.Manga 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.data.image.coil.loadManga
import eu.kanade.tachiyomi.databinding.EditMangaDialogBinding import eu.kanade.tachiyomi.databinding.EditMangaDialogBinding
import eu.kanade.tachiyomi.source.LocalSource import eu.kanade.tachiyomi.source.LocalSource
@ -176,10 +176,10 @@ class EditMangaDialog : DialogController {
binding.resetCover.isVisible = !isLocal binding.resetCover.isVisible = !isLocal
binding.resetCover.setOnClickListener { binding.resetCover.setOnClickListener {
binding.mangaCover.loadAny( binding.mangaCover.load(
manga, manga,
builder = { builder = {
parameters(Parameters.Builder().set(MangaFetcher.realCover, true).build()) parameters(Parameters.Builder().set(MangaCoverFetcher.useCustomCover, false).build())
}, },
) )
customCoverUri = null customCoverUri = null
@ -288,7 +288,7 @@ class EditMangaDialog : DialogController {
fun updateCover(uri: Uri) { fun updateCover(uri: Uri) {
willResetCover = false willResetCover = false
binding.mangaCover.loadAny(uri) binding.mangaCover.load(uri)
customCoverUri = uri customCoverUri = uri
} }

View file

@ -9,7 +9,6 @@ import coil.imageLoader
import coil.memory.MemoryCache import coil.memory.MemoryCache
import coil.request.CachePolicy import coil.request.CachePolicy
import coil.request.ImageRequest import coil.request.ImageRequest
import coil.request.Parameters
import coil.request.SuccessResult import coil.request.SuccessResult
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.cache.CoverCache 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.DownloadManager
import eu.kanade.tachiyomi.data.download.model.Download import eu.kanade.tachiyomi.data.download.model.Download
import eu.kanade.tachiyomi.data.download.model.DownloadQueue 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.CustomMangaManager
import eu.kanade.tachiyomi.data.library.LibraryServiceListener import eu.kanade.tachiyomi.data.library.LibraryServiceListener
import eu.kanade.tachiyomi.data.library.LibraryUpdateService import eu.kanade.tachiyomi.data.library.LibraryUpdateService
@ -354,14 +352,11 @@ class MangaDetailsPresenter(
val request = val request =
ImageRequest.Builder(preferences.context).data(manga) ImageRequest.Builder(preferences.context).data(manga)
.memoryCachePolicy(CachePolicy.DISABLED) .memoryCachePolicy(CachePolicy.DISABLED)
.parameters( .diskCachePolicy(CachePolicy.WRITE_ONLY)
Parameters.Builder().set(MangaFetcher.onlyFetchRemotely, true)
.build(),
)
.build() .build()
if (Coil.imageLoader(preferences.context).execute(request) is SuccessResult) { 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) { withContext(Dispatchers.Main) {
controller?.setPaletteColor() controller?.setPaletteColor()
} }

View file

@ -2,7 +2,7 @@ package eu.kanade.tachiyomi.ui.manga.track
import android.view.View import android.view.View
import androidx.core.view.isVisible import androidx.core.view.isVisible
import coil.clear import coil.dispose
import coil.load import coil.load
import com.google.android.material.shape.CornerFamily import com.google.android.material.shape.CornerFamily
import com.mikepenz.fastadapter.FastAdapter import com.mikepenz.fastadapter.FastAdapter
@ -45,7 +45,7 @@ class TrackSearchItem(val trackSearch: TrackSearch) : AbstractItem<TrackSearchIt
binding.trackSearchTitle.text = track.title binding.trackSearchTitle.text = track.title
binding.trackSearchSummary.text = track.summary binding.trackSearchSummary.text = track.summary
binding.trackSearchSummary.isVisible = track.summary.isNotBlank() binding.trackSearchSummary.isVisible = track.summary.isNotBlank()
binding.trackSearchCover.clear() binding.trackSearchCover.dispose()
if (track.cover_url.isNotEmpty()) { if (track.cover_url.isNotEmpty()) {
binding.trackSearchCover.load(track.cover_url) binding.trackSearchCover.load(track.cover_url)
} }

View file

@ -2,7 +2,7 @@ package eu.kanade.tachiyomi.ui.migration
import android.view.View import android.view.View
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import coil.clear import coil.dispose
import eu.davidea.flexibleadapter.FlexibleAdapter import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.IFlexible import eu.davidea.flexibleadapter.items.IFlexible
import eu.kanade.tachiyomi.data.image.coil.loadManga import eu.kanade.tachiyomi.data.image.coil.loadManga
@ -28,7 +28,7 @@ class MangaHolder(
binding.subtitle.text = "" binding.subtitle.text = ""
// Update the cover. // Update the cover.
binding.coverThumbnail.clear() binding.coverThumbnail.dispose()
binding.coverThumbnail.loadManga(item.manga) binding.coverThumbnail.loadManga(item.manga)
} }
} }

View file

@ -11,6 +11,7 @@ import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.DatabaseHelper import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.image.coil.CoverViewTarget 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.databinding.MangaGridItemBinding
import eu.kanade.tachiyomi.databinding.MigrationProcessItemBinding import eu.kanade.tachiyomi.databinding.MigrationProcessItemBinding
import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.source.Source
@ -143,7 +144,9 @@ class MigrationProcessHolder(
progress.isVisible = false progress.isVisible = false
val request = ImageRequest.Builder(view.context).data(manga) val request = ImageRequest.Builder(view.context).data(manga)
.target(CoverViewTarget(coverThumbnail, progress)).build() .target(CoverViewTarget(coverThumbnail, progress))
.setParameter(MangaCoverFetcher.useCustomCover, false)
.build()
Coil.imageLoader(view.context).enqueue(request) Coil.imageLoader(view.context).enqueue(request)
compactTitle.isVisible = true compactTitle.isVisible = true

View file

@ -16,7 +16,7 @@ import androidx.annotation.CallSuper
import androidx.annotation.StyleRes import androidx.annotation.StyleRes
import androidx.appcompat.widget.AppCompatImageView import androidx.appcompat.widget.AppCompatImageView
import androidx.core.view.isVisible import androidx.core.view.isVisible
import coil.clear import coil.dispose
import coil.imageLoader import coil.imageLoader
import coil.request.CachePolicy import coil.request.CachePolicy
import coil.request.ImageRequest import coil.request.ImageRequest
@ -98,7 +98,7 @@ open class ReaderPageImageView @JvmOverloads constructor(
fun recycle() = pageView?.let { fun recycle() = pageView?.let {
when (it) { when (it) {
is SubsamplingScaleImageView -> it.recycle() is SubsamplingScaleImageView -> it.recycle()
is AppCompatImageView -> it.clear() is AppCompatImageView -> it.dispose()
} }
it.isVisible = false it.isVisible = false
} }

View file

@ -23,7 +23,7 @@ import android.widget.LinearLayout
import android.widget.TextView import android.widget.TextView
import androidx.core.net.toUri import androidx.core.net.toUri
import androidx.core.view.isVisible import androidx.core.view.isVisible
import coil.loadAny import coil.load
import coil.request.CachePolicy import coil.request.CachePolicy
import com.davemorrissey.labs.subscaleview.ImageSource import com.davemorrissey.labs.subscaleview.ImageSource
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
@ -954,7 +954,7 @@ class PagerPageHolder(
* Extension method to set a [stream] into this ImageView. * Extension method to set a [stream] into this ImageView.
*/ */
private fun ImageView.setImage(stream: InputStream) { private fun ImageView.setImage(stream: InputStream) {
this.loadAny(stream.readBytes()) { this.load(stream.readBytes()) {
memoryCachePolicy(CachePolicy.DISABLED) memoryCachePolicy(CachePolicy.DISABLED)
diskCachePolicy(CachePolicy.DISABLED) diskCachePolicy(CachePolicy.DISABLED)
target(GifViewTarget(this@setImage, progressBar, decodeErrorLayout)) target(GifViewTarget(this@setImage, progressBar, decodeErrorLayout))

View file

@ -5,12 +5,13 @@ import android.view.View
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import coil.Coil import coil.Coil
import coil.clear import coil.dispose
import coil.request.ImageRequest import coil.request.ImageRequest
import eu.davidea.flexibleadapter.FlexibleAdapter import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.IFlexible import eu.davidea.flexibleadapter.items.IFlexible
import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.image.coil.CoverViewTarget 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.databinding.MangaGridItemBinding
import eu.kanade.tachiyomi.ui.library.LibraryCategoryAdapter import eu.kanade.tachiyomi.ui.library.LibraryCategoryAdapter
import eu.kanade.tachiyomi.util.view.setCards import eu.kanade.tachiyomi.util.view.setCards
@ -61,11 +62,13 @@ class BrowseSourceGridHolder(
override fun setImage(manga: Manga) { override fun setImage(manga: Manga) {
if ((view.context as? Activity)?.isDestroyed == true) return if ((view.context as? Activity)?.isDestroyed == true) return
if (manga.thumbnail_url == null) { if (manga.thumbnail_url == null) {
binding.coverThumbnail.clear() binding.coverThumbnail.dispose()
} else { } else {
manga.id ?: return manga.id ?: return
val request = ImageRequest.Builder(view.context).data(manga) 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) Coil.imageLoader(view.context).enqueue(request)
} }
} }

View file

@ -4,12 +4,13 @@ import android.view.View
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import coil.Coil import coil.Coil
import coil.clear import coil.dispose
import coil.request.ImageRequest import coil.request.ImageRequest
import eu.davidea.flexibleadapter.FlexibleAdapter import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.IFlexible import eu.davidea.flexibleadapter.items.IFlexible
import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.image.coil.CoverViewTarget 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.databinding.MangaListItemBinding
import eu.kanade.tachiyomi.util.view.setCards import eu.kanade.tachiyomi.util.view.setCards
@ -50,11 +51,13 @@ class BrowseSourceListHolder(
override fun setImage(manga: Manga) { override fun setImage(manga: Manga) {
// Update the cover. // Update the cover.
if (manga.thumbnail_url == null) { if (manga.thumbnail_url == null) {
binding.coverThumbnail.clear() binding.coverThumbnail.dispose()
} else { } else {
manga.id ?: return manga.id ?: return
val request = ImageRequest.Builder(view.context).data(manga) 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) Coil.imageLoader(view.context).enqueue(request)
} }
} }

View file

@ -4,11 +4,12 @@ import android.graphics.drawable.RippleDrawable
import android.view.View import android.view.View
import androidx.core.view.isVisible import androidx.core.view.isVisible
import coil.Coil import coil.Coil
import coil.clear import coil.dispose
import coil.request.CachePolicy import coil.request.CachePolicy
import coil.request.ImageRequest import coil.request.ImageRequest
import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.image.coil.CoverViewTarget 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.databinding.SourceGlobalSearchControllerCardItemBinding
import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder
import eu.kanade.tachiyomi.util.system.dpToPx import eu.kanade.tachiyomi.util.system.dpToPx
@ -50,12 +51,14 @@ class GlobalSearchMangaHolder(view: View, adapter: GlobalSearchCardAdapter) :
} }
fun setImage(manga: Manga) { fun setImage(manga: Manga) {
binding.itemImage.clear() binding.itemImage.dispose()
if (!manga.thumbnail_url.isNullOrEmpty()) { if (!manga.thumbnail_url.isNullOrEmpty()) {
val request = ImageRequest.Builder(itemView.context).data(manga) val request = ImageRequest.Builder(itemView.context).data(manga)
.placeholder(android.R.color.transparent) .placeholder(android.R.color.transparent)
.memoryCachePolicy(CachePolicy.DISABLED) .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) Coil.imageLoader(itemView.context).enqueue(request)
} }
} }

View file

@ -1,9 +1,14 @@
package eu.kanade.tachiyomi.util.manga package eu.kanade.tachiyomi.util.manga
import android.graphics.BitmapFactory
import androidx.annotation.ColorInt 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.database.models.Manga
import eu.kanade.tachiyomi.data.image.coil.getBestColor
import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
import java.io.File
import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentHashMap
/** Object that holds info about a covers size ratio + dominant colors */ /** Object that holds info about a covers size ratio + dominant colors */
@ -11,6 +16,7 @@ object MangaCoverMetadata {
private var coverRatioMap = ConcurrentHashMap<Long, Float>() private var coverRatioMap = ConcurrentHashMap<Long, Float>()
private var coverColorMap = ConcurrentHashMap<Long, Pair<Int, Int>>() private var coverColorMap = ConcurrentHashMap<Long, Pair<Int, Int>>()
val preferences by injectLazy<PreferencesHelper>() val preferences by injectLazy<PreferencesHelper>()
val coverCache by injectLazy<CoverCache>()
fun load() { fun load() {
val ratios = preferences.coverRatios().get() 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) { fun remove(manga: Manga) {
val id = manga.id ?: return val id = manga.id ?: return
coverRatioMap.remove(id) coverRatioMap.remove(id)