mirror of
https://github.com/null2264/yokai.git
synced 2025-06-21 10:44:42 +00:00
Coil 2.x upgrade
Co-Authored-By: Ivan Iskandar <12537387+ivaniskandar@users.noreply.github.com>
This commit is contained in:
parent
cd1e3e1f11
commit
bc778347fd
27 changed files with 533 additions and 346 deletions
|
@ -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",
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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<ActivityManager>()!!.isLowRamDevice)
|
||||
.allowHardware(true)
|
||||
.componentRegistry {
|
||||
if (Build.VERSION.SDK_INT >= 28) {
|
||||
add(ImageDecoderDecoder(context))
|
||||
val imageLoader = ImageLoader.Builder(context).apply {
|
||||
val callFactoryInit = { Injekt.get<NetworkHelper>().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<NetworkHelper>().coilClient)
|
||||
.build()
|
||||
|
||||
callFactory(callFactoryInit)
|
||||
diskCache(diskCacheInit)
|
||||
memoryCache { MemoryCache.Builder(context).maxSizePercent(0.40).build() }
|
||||
crossfade(true)
|
||||
allowRgb565(context.getSystemService<ActivityManager>()!!.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 }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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!!)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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<TrackSearchIt
|
|||
binding.trackSearchTitle.text = track.title
|
||||
binding.trackSearchSummary.text = track.summary
|
||||
binding.trackSearchSummary.isVisible = track.summary.isNotBlank()
|
||||
binding.trackSearchCover.clear()
|
||||
binding.trackSearchCover.dispose()
|
||||
if (track.cover_url.isNotEmpty()) {
|
||||
binding.trackSearchCover.load(track.cover_url)
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@ package eu.kanade.tachiyomi.ui.migration
|
|||
|
||||
import android.view.View
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import coil.clear
|
||||
import coil.dispose
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
import eu.davidea.flexibleadapter.items.IFlexible
|
||||
import eu.kanade.tachiyomi.data.image.coil.loadManga
|
||||
|
@ -28,7 +28,7 @@ class MangaHolder(
|
|||
binding.subtitle.text = ""
|
||||
|
||||
// Update the cover.
|
||||
binding.coverThumbnail.clear()
|
||||
binding.coverThumbnail.dispose()
|
||||
binding.coverThumbnail.loadManga(item.manga)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,6 +11,7 @@ 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.CoverViewTarget
|
||||
import eu.kanade.tachiyomi.data.image.coil.MangaCoverFetcher
|
||||
import eu.kanade.tachiyomi.databinding.MangaGridItemBinding
|
||||
import eu.kanade.tachiyomi.databinding.MigrationProcessItemBinding
|
||||
import eu.kanade.tachiyomi.source.Source
|
||||
|
@ -143,7 +144,9 @@ class MigrationProcessHolder(
|
|||
progress.isVisible = false
|
||||
|
||||
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)
|
||||
|
||||
compactTitle.isVisible = true
|
||||
|
|
|
@ -16,7 +16,7 @@ import androidx.annotation.CallSuper
|
|||
import androidx.annotation.StyleRes
|
||||
import androidx.appcompat.widget.AppCompatImageView
|
||||
import androidx.core.view.isVisible
|
||||
import coil.clear
|
||||
import coil.dispose
|
||||
import coil.imageLoader
|
||||
import coil.request.CachePolicy
|
||||
import coil.request.ImageRequest
|
||||
|
@ -98,7 +98,7 @@ open class ReaderPageImageView @JvmOverloads constructor(
|
|||
fun recycle() = pageView?.let {
|
||||
when (it) {
|
||||
is SubsamplingScaleImageView -> it.recycle()
|
||||
is AppCompatImageView -> it.clear()
|
||||
is AppCompatImageView -> it.dispose()
|
||||
}
|
||||
it.isVisible = false
|
||||
}
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<Long, Float>()
|
||||
private var coverColorMap = ConcurrentHashMap<Long, Pair<Int, Int>>()
|
||||
val preferences by injectLazy<PreferencesHelper>()
|
||||
val coverCache by injectLazy<CoverCache>()
|
||||
|
||||
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)
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue