refactor(cover): Adjust cover (memory) cache key

Hopefully fix library image flickering on resume/bind
This commit is contained in:
Ahmad Ansori Palembani 2024-08-17 11:08:50 +07:00
parent df66327996
commit 653b2d7839
Signed by: null2264
GPG key ID: BA64F8B60AF3EFB6
10 changed files with 62 additions and 48 deletions

View file

@ -53,6 +53,7 @@ import eu.kanade.tachiyomi.util.system.AuthenticatorUtil
import eu.kanade.tachiyomi.util.system.launchIO import eu.kanade.tachiyomi.util.system.launchIO
import eu.kanade.tachiyomi.util.system.localeContext import eu.kanade.tachiyomi.util.system.localeContext
import eu.kanade.tachiyomi.util.system.notification import eu.kanade.tachiyomi.util.system.notification
import java.security.Security
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
@ -69,7 +70,6 @@ import yokai.core.migration.migrations.migrations
import yokai.domain.base.BasePreferences import yokai.domain.base.BasePreferences
import yokai.i18n.MR import yokai.i18n.MR
import yokai.util.lang.getString import yokai.util.lang.getString
import java.security.Security
open class App : Application(), DefaultLifecycleObserver, SingletonImageLoader.Factory { open class App : Application(), DefaultLifecycleObserver, SingletonImageLoader.Factory {
@ -251,7 +251,7 @@ open class App : Application(), DefaultLifecycleObserver, SingletonImageLoader.F
} }
diskCache(diskCacheLazy::value) diskCache(diskCacheLazy::value)
// memoryCache { MemoryCache.Builder().maxSizePercent(this@App, 0.40).build() } //memoryCache { MemoryCache.Builder().maxSizePercent(this@App, 0.40).build() }
crossfade(true) crossfade(true)
allowRgb565(this@App.getSystemService<ActivityManager>()!!.isLowRamDevice) allowRgb565(this@App.getSystemService<ActivityManager>()!!.isLowRamDevice)
allowHardware(true) allowHardware(true)

View file

@ -6,7 +6,6 @@ import co.touchlab.kermit.Logger
import coil3.imageLoader import coil3.imageLoader
import coil3.memory.MemoryCache import coil3.memory.MemoryCache
import eu.kanade.tachiyomi.data.database.DatabaseHelper import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.data.database.models.MangaImpl
import eu.kanade.tachiyomi.domain.manga.models.Manga import eu.kanade.tachiyomi.domain.manga.models.Manga
import eu.kanade.tachiyomi.util.storage.DiskUtil import eu.kanade.tachiyomi.util.storage.DiskUtil
import eu.kanade.tachiyomi.util.system.e import eu.kanade.tachiyomi.util.system.e
@ -14,16 +13,16 @@ import eu.kanade.tachiyomi.util.system.executeOnIO
import eu.kanade.tachiyomi.util.system.toast import eu.kanade.tachiyomi.util.system.toast
import eu.kanade.tachiyomi.util.system.withIOContext import eu.kanade.tachiyomi.util.system.withIOContext
import eu.kanade.tachiyomi.util.system.withUIContext import eu.kanade.tachiyomi.util.system.withUIContext
import java.io.File
import java.io.IOException
import java.io.InputStream
import java.util.concurrent.*
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import yokai.i18n.MR import yokai.i18n.MR
import yokai.util.lang.getString import yokai.util.lang.getString
import java.io.File
import java.io.IOException
import java.io.InputStream
import java.util.concurrent.*
/** /**
* Class used to create cover cache. * Class used to create cover cache.
@ -171,7 +170,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())) removeFromMemory(manga, true)
} }
} }
@ -185,7 +184,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())) removeFromMemory(manga, true)
return result return result
} }
@ -195,18 +194,28 @@ class CoverCache(val context: Context) {
* @param thumbnailUrl the thumbnail url. * @param thumbnailUrl the thumbnail url.
* @return cover image. * @return cover image.
*/ */
fun getCoverFile(manga: Manga): File { fun getCoverFile(mangaThumbnailUrl: String?, isOnline: Boolean = false): File? {
val hashKey = DiskUtil.hashKeyForDisk((manga.thumbnail_url.orEmpty())) return mangaThumbnailUrl?.let {
return if (manga.favorite) { File(if (!isOnline) cacheDir else onlineCoverDirectory, DiskUtil.hashKeyForDisk(it))
File(cacheDir, hashKey) }
} else { }
File(onlineCoverDirectory, hashKey)
fun removeFromMemory(manga: Manga, custom: Boolean = false) {
if (custom) {
context.imageLoader.memoryCache?.remove(MemoryCache.Key(manga.key()))
return
}
manga.thumbnail_url?.let {
if (it.isEmpty()) return
context.imageLoader.memoryCache
?.remove(MemoryCache.Key(if (!manga.favorite) it else DiskUtil.hashKeyForDisk(it)))
} }
} }
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(name, true) ?: return
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()
} }
@ -225,12 +234,11 @@ class CoverCache(val context: Context) {
if (manga.thumbnail_url.isNullOrEmpty()) return if (manga.thumbnail_url.isNullOrEmpty()) return
// Remove file // Remove file
val file = getCoverFile(manga) getCoverFile(manga.thumbnail_url, !manga.favorite)?.let {
if (deleteCustom) deleteCustomCover(manga) removeFromMemory(manga)
if (file.exists()) { it.delete()
context.imageLoader.memoryCache?.remove(MemoryCache.Key(manga.key()))
file.delete()
} }
if (deleteCustom) deleteCustomCover(manga)
} }
private fun getCacheDir(dir: String): File { private fun getCacheDir(dir: String): File {

View file

@ -6,7 +6,6 @@ import androidx.palette.graphics.Palette
import coil3.Image import coil3.Image
import coil3.ImageLoader import coil3.ImageLoader
import coil3.imageLoader import coil3.imageLoader
import coil3.memory.MemoryCache
import coil3.request.Disposable import coil3.request.Disposable
import coil3.request.ImageRequest import coil3.request.ImageRequest
import coil3.target.ImageViewTarget import coil3.target.ImageViewTarget
@ -26,15 +25,15 @@ class LibraryMangaImageTarget(
super.onError(error) super.onError(error)
if (manga.favorite) { if (manga.favorite) {
launchIO { launchIO {
val file = coverCache.getCoverFile(manga) val file = coverCache.getCoverFile(manga.thumbnail_url, false)
// if the file exists and the there was still an error then the file is corrupted // if the file exists and the there was still an error then the file is corrupted
if (file.exists()) { if (file != null && file.exists()) {
val options = BitmapFactory.Options() val options = BitmapFactory.Options()
options.inJustDecodeBounds = true options.inJustDecodeBounds = true
BitmapFactory.decodeFile(file.path, options) BitmapFactory.decodeFile(file.path, options)
if (options.outWidth == -1 || options.outHeight == -1) { if (options.outWidth == -1 || options.outHeight == -1) {
coverCache.removeFromMemory(manga)
file.delete() file.delete()
view.context.imageLoader.memoryCache?.remove(MemoryCache.Key(manga.key()))
} }
} }
} }
@ -52,7 +51,6 @@ inline fun ImageView.loadManga(
.data(manga) .data(manga)
.target(LibraryMangaImageTarget(this, manga)) .target(LibraryMangaImageTarget(this, manga))
.apply(builder) .apply(builder)
.memoryCacheKey(manga.key())
.build() .build()
return imageLoader.enqueue(request) return imageLoader.enqueue(request)
} }

View file

@ -87,8 +87,8 @@ class MangaCoverFetcher(
val customCoverLoader = tryCustomCover() val customCoverLoader = tryCustomCover()
if (customCoverLoader != null) return customCoverLoader if (customCoverLoader != null) return customCoverLoader
} }
val coverFile = coverCache.getCoverFile(manga) val coverFile = coverCache.getCoverFile(manga.thumbnail_url, !manga.favorite)
if (!shouldFetchRemotely && coverFile.exists() && options.diskCachePolicy.readEnabled) { if (!shouldFetchRemotely && coverFile != null && coverFile.exists() && options.diskCachePolicy.readEnabled) {
if (!manga.favorite) { if (!manga.favorite) {
coverFile.setLastModified(Date().time) coverFile.setLastModified(Date().time)
} }

View file

@ -2,12 +2,16 @@ package eu.kanade.tachiyomi.data.coil
import coil3.key.Keyer import coil3.key.Keyer
import coil3.request.Options import coil3.request.Options
import eu.kanade.tachiyomi.data.database.models.hasCustomCover
import eu.kanade.tachiyomi.domain.manga.models.Manga import eu.kanade.tachiyomi.domain.manga.models.Manga
import eu.kanade.tachiyomi.util.storage.DiskUtil import eu.kanade.tachiyomi.util.storage.DiskUtil
class MangaCoverKeyer : Keyer<Manga> { class MangaCoverKeyer : Keyer<Manga> {
override fun key(data: Manga, options: Options): String? { override fun key(data: Manga, options: Options): String? {
if (data.thumbnail_url.isNullOrBlank()) return null val hasCustomCover by lazy { data.hasCustomCover() }
if (data.thumbnail_url.isNullOrBlank() && !hasCustomCover) return null
if (hasCustomCover) return data.key()
return if (!data.favorite) { return if (!data.favorite) {
data.thumbnail_url!! data.thumbnail_url!!
} else { } else {

View file

@ -1,6 +1,7 @@
package eu.kanade.tachiyomi.data.database.models package eu.kanade.tachiyomi.data.database.models
import android.content.Context import android.content.Context
import eu.kanade.tachiyomi.data.cache.CoverCache
import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.domain.manga.models.Manga import eu.kanade.tachiyomi.domain.manga.models.Manga
import eu.kanade.tachiyomi.domain.manga.models.Manga.Companion.TYPE_COMIC import eu.kanade.tachiyomi.domain.manga.models.Manga.Companion.TYPE_COMIC
@ -14,6 +15,7 @@ import eu.kanade.tachiyomi.ui.reader.settings.OrientationType
import eu.kanade.tachiyomi.ui.reader.settings.ReadingModeType import eu.kanade.tachiyomi.ui.reader.settings.ReadingModeType
import eu.kanade.tachiyomi.util.manga.MangaCoverMetadata import eu.kanade.tachiyomi.util.manga.MangaCoverMetadata
import eu.kanade.tachiyomi.util.system.withIOContext import eu.kanade.tachiyomi.util.system.withIOContext
import java.util.Locale
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
@ -21,7 +23,6 @@ import yokai.data.updateStrategyAdapter
import yokai.domain.chapter.interactor.GetChapter import yokai.domain.chapter.interactor.GetChapter
import yokai.i18n.MR import yokai.i18n.MR
import yokai.util.lang.getString import yokai.util.lang.getString
import java.util.*
fun Manga.sortDescending(preferences: PreferencesHelper): Boolean = fun Manga.sortDescending(preferences: PreferencesHelper): Boolean =
if (usesLocalSort) sortDescending else preferences.chaptersDescAsDefault().get() if (usesLocalSort) sortDescending else preferences.chaptersDescAsDefault().get()
@ -221,3 +222,7 @@ fun Manga.Companion.mapper(
this.filtered_scanlators = filteredScanlators this.filtered_scanlators = filteredScanlators
this.update_strategy = updateStrategy.let(updateStrategyAdapter::decode) this.update_strategy = updateStrategy.let(updateStrategyAdapter::decode)
} }
fun Manga.hasCustomCover(coverCache: CoverCache = Injekt.get()): Boolean {
return coverCache.getCustomCoverFile(this).exists()
}

View file

@ -1646,8 +1646,8 @@ class LibraryPresenter(
libraryManga.forEach { manga -> libraryManga.forEach { manga ->
if (manga.id == null) return@forEach if (manga.id == null) return@forEach
if (manga.thumbnail_url?.startsWith("custom", ignoreCase = true) == true) { if (manga.thumbnail_url?.startsWith("custom", ignoreCase = true) == true) {
val file = cc.getCoverFile(manga) val file = cc.getCoverFile(manga.thumbnail_url, !manga.favorite)
if (file.exists()) { if (file != null && file.exists()) {
file.renameTo(cc.getCustomCoverFile(manga)) file.renameTo(cc.getCustomCoverFile(manga))
} }
manga.thumbnail_url = manga.thumbnail_url =

View file

@ -136,6 +136,11 @@ import eu.kanade.tachiyomi.util.view.snack
import eu.kanade.tachiyomi.util.view.toolbarHeight import eu.kanade.tachiyomi.util.view.toolbarHeight
import eu.kanade.tachiyomi.util.view.withFadeTransaction import eu.kanade.tachiyomi.util.view.withFadeTransaction
import eu.kanade.tachiyomi.widget.LinearLayoutManagerAccurateOffset import eu.kanade.tachiyomi.widget.LinearLayoutManagerAccurateOffset
import java.io.File
import java.io.IOException
import java.util.Locale
import kotlin.math.max
import kotlin.math.roundToInt
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withTimeoutOrNull import kotlinx.coroutines.withTimeoutOrNull
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
@ -143,11 +148,6 @@ import uy.kohesive.injekt.api.get
import yokai.i18n.MR import yokai.i18n.MR
import yokai.presentation.core.Constants import yokai.presentation.core.Constants
import yokai.util.lang.getString import yokai.util.lang.getString
import java.io.File
import java.io.IOException
import java.util.*
import kotlin.math.max
import kotlin.math.roundToInt
import android.R as AR import android.R as AR
class MangaDetailsController : class MangaDetailsController :
@ -580,7 +580,6 @@ class MangaDetailsController :
val view = view ?: return val view = view ?: return
val request = ImageRequest.Builder(view.context).data(presenter.manga).allowHardware(false) val request = ImageRequest.Builder(view.context).data(presenter.manga).allowHardware(false)
.memoryCacheKey(presenter.manga.key())
.target( .target(
onSuccess = { image -> onSuccess = { image ->
val drawable = image.asDrawable(view.context.resources) val drawable = image.asDrawable(view.context.resources)
@ -619,8 +618,8 @@ class MangaDetailsController :
getHeader()?.updateCover(manga!!) getHeader()?.updateCover(manga!!)
}, },
onError = { onError = {
val file = presenter.coverCache.getCoverFile(manga!!) val file = presenter.coverCache.getCoverFile(manga!!.thumbnail_url, !manga!!.favorite)
if (file.exists()) { if (file != null && file.exists()) {
file.delete() file.delete()
setPaletteColor() setPaletteColor()
} }

View file

@ -5,7 +5,6 @@ import android.graphics.Bitmap
import android.net.Uri import android.net.Uri
import androidx.core.net.toFile import androidx.core.net.toFile
import coil3.imageLoader import coil3.imageLoader
import coil3.memory.MemoryCache
import coil3.request.CachePolicy import coil3.request.CachePolicy
import coil3.request.ImageRequest import coil3.request.ImageRequest
import coil3.request.SuccessResult import coil3.request.SuccessResult
@ -21,6 +20,7 @@ import eu.kanade.tachiyomi.data.database.models.Track
import eu.kanade.tachiyomi.data.database.models.bookmarkedFilter import eu.kanade.tachiyomi.data.database.models.bookmarkedFilter
import eu.kanade.tachiyomi.data.database.models.chapterOrder import eu.kanade.tachiyomi.data.database.models.chapterOrder
import eu.kanade.tachiyomi.data.database.models.downloadedFilter import eu.kanade.tachiyomi.data.database.models.downloadedFilter
import eu.kanade.tachiyomi.data.database.models.hasCustomCover
import eu.kanade.tachiyomi.data.database.models.readFilter import eu.kanade.tachiyomi.data.database.models.readFilter
import eu.kanade.tachiyomi.data.database.models.sortDescending import eu.kanade.tachiyomi.data.database.models.sortDescending
import eu.kanade.tachiyomi.data.download.DownloadManager import eu.kanade.tachiyomi.data.download.DownloadManager
@ -403,7 +403,7 @@ class MangaDetailsPresenter(
.build() .build()
if (preferences.context.imageLoader.execute(request) is SuccessResult) { if (preferences.context.imageLoader.execute(request) is SuccessResult) {
preferences.context.imageLoader.memoryCache?.remove(MemoryCache.Key(manga.key())) coverCache.removeFromMemory(manga, manga.hasCustomCover(coverCache))
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
view?.setPaletteColor() view?.setPaletteColor()
} }
@ -920,8 +920,8 @@ class MangaDetailsPresenter(
} }
private fun saveCover(directory: UniFile): UniFile { private fun saveCover(directory: UniFile): UniFile {
val cover = coverCache.getCustomCoverFile(manga).takeIf { it.exists() } ?: coverCache.getCoverFile(manga) val cover = coverCache.getCustomCoverFile(manga).takeIf { it.exists() } ?: coverCache.getCoverFile(manga.thumbnail_url, !manga.favorite)
val type = ImageUtil.findImageType(cover.inputStream()) val type = cover?.let { ImageUtil.findImageType(it.inputStream()) }
?: throw Exception("Not an image") ?: throw Exception("Not an image")
// Build destination file. // Build destination file.

View file

@ -8,9 +8,9 @@ import eu.kanade.tachiyomi.data.coil.getBestColor
import eu.kanade.tachiyomi.data.database.models.dominantCoverColors import eu.kanade.tachiyomi.data.database.models.dominantCoverColors
import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.domain.manga.models.Manga import eu.kanade.tachiyomi.domain.manga.models.Manga
import uy.kohesive.injekt.injectLazy
import java.io.File import java.io.File
import java.util.concurrent.* import java.util.concurrent.ConcurrentHashMap
import uy.kohesive.injekt.injectLazy
/** Object that holds info about a covers size ratio + dominant colors */ /** Object that holds info about a covers size ratio + dominant colors */
object MangaCoverMetadata { object MangaCoverMetadata {
@ -54,9 +54,9 @@ object MangaCoverMetadata {
remove(manga) remove(manga)
} }
if (manga.vibrantCoverColor != null && !manga.favorite) return if (manga.vibrantCoverColor != null && !manga.favorite) return
val file = ogFile ?: coverCache.getCustomCoverFile(manga).takeIf { it.exists() } ?: coverCache.getCoverFile(manga) val file = ogFile ?: coverCache.getCustomCoverFile(manga).takeIf { it.exists() } ?: coverCache.getCoverFile(manga.thumbnail_url, !manga.favorite)
// if the file exists and the there was still an error then the file is corrupted // if the file exists and the there was still an error then the file is corrupted
if (file.exists()) { if (file != null && file.exists()) {
val options = BitmapFactory.Options() val options = BitmapFactory.Options()
val hasVibrantColor = if (manga.favorite) manga.vibrantCoverColor != null else true val hasVibrantColor = if (manga.favorite) manga.vibrantCoverColor != null else true
if (manga.dominantCoverColors != null && hasVibrantColor && !force) { if (manga.dominantCoverColors != null && hasVibrantColor && !force) {