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")
|
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",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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.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 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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.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()
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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))
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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))
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue