refactor(cover): Data class for manga cover

This commit is contained in:
Ahmad Ansori Palembani 2024-08-18 18:38:39 +07:00
parent 8ad123956c
commit 839f762fa7
Signed by: null2264
GPG key ID: BA64F8B60AF3EFB6
20 changed files with 191 additions and 80 deletions

View file

@ -39,6 +39,7 @@ import eu.kanade.tachiyomi.core.preference.PreferenceStore
import eu.kanade.tachiyomi.data.coil.BufferedSourceFetcher import eu.kanade.tachiyomi.data.coil.BufferedSourceFetcher
import eu.kanade.tachiyomi.data.coil.MangaCoverFetcher import eu.kanade.tachiyomi.data.coil.MangaCoverFetcher
import eu.kanade.tachiyomi.data.coil.MangaCoverKeyer import eu.kanade.tachiyomi.data.coil.MangaCoverKeyer
import eu.kanade.tachiyomi.data.coil.MangaKeyer
import eu.kanade.tachiyomi.data.coil.TachiyomiImageDecoder import eu.kanade.tachiyomi.data.coil.TachiyomiImageDecoder
import eu.kanade.tachiyomi.data.notification.Notifications import eu.kanade.tachiyomi.data.notification.Notifications
import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.PreferencesHelper
@ -243,8 +244,10 @@ open class App : Application(), DefaultLifecycleObserver, SingletonImageLoader.F
add(TachiyomiImageDecoder.Factory()) add(TachiyomiImageDecoder.Factory())
// Fetcher.Factory // Fetcher.Factory
add(BufferedSourceFetcher.Factory()) add(BufferedSourceFetcher.Factory())
add(MangaCoverFetcher.Factory(callFactoryLazy)) add(MangaCoverFetcher.MangaFactory(callFactoryLazy))
add(MangaCoverFetcher.MangaCoverFactory(callFactoryLazy))
// Keyer // Keyer
add(MangaKeyer())
add(MangaCoverKeyer()) add(MangaCoverKeyer())
} }
crossfade(true) crossfade(true)

View file

@ -37,10 +37,12 @@ import eu.kanade.tachiyomi.domain.manga.models.Manga
import eu.kanade.tachiyomi.ui.recents.RecentsPresenter import eu.kanade.tachiyomi.ui.recents.RecentsPresenter
import eu.kanade.tachiyomi.util.system.dpToPx import eu.kanade.tachiyomi.util.system.dpToPx
import eu.kanade.tachiyomi.util.system.launchIO import eu.kanade.tachiyomi.util.system.launchIO
import java.util.Calendar
import java.util.Date
import kotlin.math.min
import kotlinx.coroutines.MainScope import kotlinx.coroutines.MainScope
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
import java.util.* import yokai.domain.manga.models.cover
import kotlin.math.min
class UpdatesGridGlanceWidget : GlanceAppWidget() { class UpdatesGridGlanceWidget : GlanceAppWidget() {
private val app: Application by injectLazy() private val app: Application by injectLazy()
@ -97,7 +99,7 @@ class UpdatesGridGlanceWidget : GlanceAppWidget() {
.map { it.first } .map { it.first }
.map { updatesView -> .map { updatesView ->
val request = ImageRequest.Builder(app) val request = ImageRequest.Builder(app)
.data(updatesView) .data(updatesView.cover())
.memoryCachePolicy(CachePolicy.DISABLED) .memoryCachePolicy(CachePolicy.DISABLED)
.precision(Precision.EXACT) .precision(Precision.EXACT)
.size(widthPx, heightPx) .size(widthPx, heightPx)

View file

@ -11,8 +11,6 @@ import androidx.glance.layout.ContentScale
import androidx.glance.layout.fillMaxSize import androidx.glance.layout.fillMaxSize
import androidx.glance.layout.size import androidx.glance.layout.size
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import yokai.i18n.MR
import yokai.util.lang.getString
import eu.kanade.tachiyomi.appwidget.util.appWidgetInnerRadius import eu.kanade.tachiyomi.appwidget.util.appWidgetInnerRadius
val CoverWidth = 58.dp val CoverWidth = 58.dp

View file

@ -158,8 +158,10 @@ class CoverCache(val context: Context) {
* @param manga the manga. * @param manga the manga.
* @return cover image. * @return cover image.
*/ */
fun getCustomCoverFile(manga: Manga): File { fun getCustomCoverFile(manga: Manga): File = getCustomCoverFile(manga.id)
return File(customCoverCacheDir, DiskUtil.hashKeyForDisk(manga.id.toString()))
fun getCustomCoverFile(mangaId: Long?): File {
return File(customCoverCacheDir, DiskUtil.hashKeyForDisk(mangaId.toString()))
} }
/** /**

View file

@ -11,29 +11,29 @@ import coil3.request.ImageRequest
import coil3.target.ImageViewTarget import coil3.target.ImageViewTarget
import eu.kanade.tachiyomi.data.cache.CoverCache import eu.kanade.tachiyomi.data.cache.CoverCache
import eu.kanade.tachiyomi.data.database.models.updateCoverLastModified import eu.kanade.tachiyomi.data.database.models.updateCoverLastModified
import eu.kanade.tachiyomi.domain.manga.models.Manga
import eu.kanade.tachiyomi.util.system.launchIO import eu.kanade.tachiyomi.util.system.launchIO
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
import yokai.domain.manga.models.MangaCover
class LibraryMangaImageTarget( class LibraryMangaImageTarget(
override val view: ImageView, override val view: ImageView,
val manga: Manga, val cover: MangaCover,
) : ImageViewTarget(view) { ) : ImageViewTarget(view) {
private val coverCache: CoverCache by injectLazy() private val coverCache: CoverCache by injectLazy()
override fun onError(error: Image?) { override fun onError(error: Image?) {
super.onError(error) super.onError(error)
if (manga.favorite) { if (cover.inLibrary) {
launchIO { launchIO {
val file = coverCache.getCoverFile(manga.thumbnail_url, false) val file = coverCache.getCoverFile(cover.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 != null && 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) {
manga.updateCoverLastModified() cover.updateCoverLastModified()
file.delete() file.delete()
} }
} }
@ -44,13 +44,13 @@ class LibraryMangaImageTarget(
@JvmSynthetic @JvmSynthetic
inline fun ImageView.loadManga( inline fun ImageView.loadManga(
manga: Manga, cover: MangaCover,
imageLoader: ImageLoader = context.imageLoader, imageLoader: ImageLoader = context.imageLoader,
builder: ImageRequest.Builder.() -> Unit = {}, builder: ImageRequest.Builder.() -> Unit = {},
): Disposable { ): Disposable {
val request = ImageRequest.Builder(context) val request = ImageRequest.Builder(context)
.data(manga) .data(cover)
.target(LibraryMangaImageTarget(this, manga)) .target(LibraryMangaImageTarget(this, cover))
.apply(builder) .apply(builder)
.build() .build()
return imageLoader.enqueue(request) return imageLoader.enqueue(request)

View file

@ -39,9 +39,12 @@ import okio.buffer
import okio.sink import okio.sink
import okio.source import okio.source
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
import yokai.domain.manga.models.MangaCover
class MangaCoverFetcher( class MangaCoverFetcher(
private val manga: Manga, private val mangaId: Long?,
private val url: String?,
private val isInLibrary: Boolean,
private val sourceLazy: Lazy<HttpSource?>, private val sourceLazy: Lazy<HttpSource?>,
private val options: Options, private val options: Options,
private val callFactoryLazy: Lazy<Call.Factory>, private val callFactoryLazy: Lazy<Call.Factory>,
@ -53,7 +56,6 @@ class MangaCoverFetcher(
private val diskCacheKey: String private val diskCacheKey: String
get() = diskCacheKeyLazy.value get() = diskCacheKeyLazy.value
private lateinit var url: String
private val fileScope = CoroutineScope(Job() + Dispatchers.IO) private val fileScope = CoroutineScope(Job() + Dispatchers.IO)
@ -61,21 +63,21 @@ class MangaCoverFetcher(
if (options.extras.getOrDefault(USE_CUSTOM_COVER_KEY)) { if (options.extras.getOrDefault(USE_CUSTOM_COVER_KEY)) {
val customCoverFile = customCoverFileLazy.value val customCoverFile = customCoverFileLazy.value
if (customCoverFile.exists()) { if (customCoverFile.exists()) {
setRatioAndColorsInScope(manga, UniFile.fromFile(customCoverFile)) setRatioAndColorsInScope(mangaId, url, isInLibrary, UniFile.fromFile(customCoverFile))
return fileLoader(customCoverFile) return fileLoader(customCoverFile)
} }
} }
url = manga.thumbnail_url ?: error("No cover specified") if (url == null) error("No cover specified")
return when (getResourceType(url)) { return when (getResourceType(url)) {
Type.URL -> httpLoader() Type.URL -> httpLoader()
Type.File -> { Type.File -> {
val file = File(url.substringAfter("file://")) val file = File(url.substringAfter("file://"))
setRatioAndColorsInScope(manga, UniFile.fromFile(file)) setRatioAndColorsInScope(mangaId, url, isInLibrary, UniFile.fromFile(file))
fileLoader(file) fileLoader(file)
} }
Type.URI -> { Type.URI -> {
setRatioAndColorsInScope(manga, UniFile.fromUri(options.context, url.toUri())) setRatioAndColorsInScope(mangaId, url, isInLibrary, UniFile.fromUri(options.context, url.toUri()))
fileUriLoader(url) fileUriLoader(url)
} }
null -> error("Invalid image") null -> error("Invalid image")
@ -109,10 +111,10 @@ class MangaCoverFetcher(
private suspend fun httpLoader(): FetchResult { private suspend fun httpLoader(): FetchResult {
val coverFile = coverFileLazy.value val coverFile = coverFileLazy.value
if (coverFile?.exists() == true && options.diskCachePolicy.readEnabled) { if (coverFile?.exists() == true && options.diskCachePolicy.readEnabled) {
if (!manga.favorite) { if (!isInLibrary) {
coverFile.setLastModified(Date().time) coverFile.setLastModified(Date().time)
} }
setRatioAndColorsInScope(manga, UniFile.fromFile(coverFile)) setRatioAndColorsInScope(mangaId, url, isInLibrary, UniFile.fromFile(coverFile))
return fileLoader(coverFile) return fileLoader(coverFile)
} }
@ -123,12 +125,12 @@ class MangaCoverFetcher(
val snapshotCoverCache = moveSnapshotToCoverCache(snapshot, coverFile) val snapshotCoverCache = moveSnapshotToCoverCache(snapshot, coverFile)
if (snapshotCoverCache != null) { if (snapshotCoverCache != null) {
// Read from cover cache after added to library // Read from cover cache after added to library
setRatioAndColorsInScope(manga, UniFile.fromFile(snapshotCoverCache)) setRatioAndColorsInScope(mangaId, url, isInLibrary, UniFile.fromFile(snapshotCoverCache))
return fileLoader(snapshotCoverCache) return fileLoader(snapshotCoverCache)
} }
// Read from snapshot // Read from snapshot
setRatioAndColorsInScope(manga) setRatioAndColorsInScope(mangaId, url, isInLibrary)
return SourceFetchResult( return SourceFetchResult(
source = snapshot.toImageSource(), source = snapshot.toImageSource(),
mimeType = "image/*", mimeType = "image/*",
@ -142,7 +144,7 @@ class MangaCoverFetcher(
try { try {
// Read from cover cache after library manga cover updated // Read from cover cache after library manga cover updated
val responseCoverCache = writeResponseToCoverCache(response, coverFile) val responseCoverCache = writeResponseToCoverCache(response, coverFile)
setRatioAndColorsInScope(manga) setRatioAndColorsInScope(mangaId, url, isInLibrary)
if (responseCoverCache != null) { if (responseCoverCache != null) {
return fileLoader(responseCoverCache) return fileLoader(responseCoverCache)
} }
@ -185,7 +187,7 @@ class MangaCoverFetcher(
private fun newRequest(): Request { private fun newRequest(): Request {
val request = Request.Builder().apply { val request = Request.Builder().apply {
url(url) url(url!!)
val sourceHeaders = sourceLazy.value?.headers val sourceHeaders = sourceLazy.value?.headers
if (sourceHeaders != null) if (sourceHeaders != null)
@ -293,9 +295,9 @@ class MangaCoverFetcher(
) )
} }
private fun setRatioAndColorsInScope(manga: Manga, ogFile: UniFile? = null, force: Boolean = false) { private fun setRatioAndColorsInScope(mangaId: Long?, mangaThumbnailUrl: String?, isInLibrary: Boolean, ogFile: UniFile? = null, force: Boolean = false) {
fileScope.launch { fileScope.launch {
MangaCoverMetadata.setRatioAndColors(manga, ogFile, force) MangaCoverMetadata.setRatioAndColors(mangaId, mangaThumbnailUrl, isInLibrary, ogFile, force)
} }
} }
@ -324,7 +326,7 @@ class MangaCoverFetcher(
} }
} }
class Factory( class MangaFactory(
private val callFactoryLazy: Lazy<Call.Factory>, private val callFactoryLazy: Lazy<Call.Factory>,
) : Fetcher.Factory<Manga> { ) : Fetcher.Factory<Manga> {
@ -333,7 +335,9 @@ class MangaCoverFetcher(
override fun create(data: Manga, options: Options, imageLoader: ImageLoader): Fetcher { override fun create(data: Manga, options: Options, imageLoader: ImageLoader): Fetcher {
return MangaCoverFetcher( return MangaCoverFetcher(
manga = data, mangaId = data.id,
url = data.thumbnail_url,
isInLibrary = data.favorite,
sourceLazy = lazy { sourceManager.get(data.source) as? HttpSource }, sourceLazy = lazy { sourceManager.get(data.source) as? HttpSource },
options = options, options = options,
callFactoryLazy = callFactoryLazy, callFactoryLazy = callFactoryLazy,
@ -345,6 +349,29 @@ class MangaCoverFetcher(
} }
} }
class MangaCoverFactory(
private val callFactoryLazy: Lazy<Call.Factory>,
) : Fetcher.Factory<MangaCover> {
private val coverCache: CoverCache by injectLazy()
private val sourceManager: SourceManager by injectLazy()
override fun create(data: MangaCover, options: Options, imageLoader: ImageLoader): Fetcher {
return MangaCoverFetcher(
mangaId = data.mangaId,
url = data.url,
isInLibrary = data.inLibrary,
sourceLazy = lazy { sourceManager.get(data.sourceId) as? HttpSource },
options = options,
callFactoryLazy = callFactoryLazy,
diskCacheKeyLazy = lazy { imageLoader.components.key(data, options)!! },
coverFileLazy = lazy { coverCache.getCoverFile(data.url, !data.inLibrary) },
customCoverFileLazy = lazy { coverCache.getCustomCoverFile(data.mangaId) },
imageLoader = imageLoader,
)
}
}
private enum class Type { private enum class Type {
File, URL, URI; File, URL, URI;
} }

View file

@ -2,11 +2,15 @@ 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.cache.CoverCache
import eu.kanade.tachiyomi.data.database.models.hasCustomCover 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
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import yokai.domain.manga.models.MangaCover
class MangaCoverKeyer : Keyer<Manga> { class MangaKeyer : Keyer<Manga> {
override fun key(data: Manga, options: Options): String { override fun key(data: Manga, options: Options): String {
val key = when { val key = when {
data.hasCustomCover() -> data.id data.hasCustomCover() -> data.id
@ -17,3 +21,15 @@ class MangaCoverKeyer : Keyer<Manga> {
return "${key};${data.cover_last_modified}" return "${key};${data.cover_last_modified}"
} }
} }
class MangaCoverKeyer(private val coverCache: CoverCache = Injekt.get()) : Keyer<MangaCover> {
override fun key(data: MangaCover, options: Options): String {
val key = when {
coverCache.getCustomCoverFile(data.mangaId).exists() -> data.mangaId
data.inLibrary -> DiskUtil.hashKeyForDisk(data.url)
else -> data.url
}
return "${key};${data.lastModified}"
}
}

View file

@ -23,6 +23,7 @@ import uy.kohesive.injekt.injectLazy
import yokai.data.updateStrategyAdapter import yokai.data.updateStrategyAdapter
import yokai.domain.chapter.interactor.GetChapter import yokai.domain.chapter.interactor.GetChapter
import yokai.domain.manga.interactor.UpdateManga import yokai.domain.manga.interactor.UpdateManga
import yokai.domain.manga.models.MangaCover
import yokai.domain.manga.models.MangaUpdate import yokai.domain.manga.models.MangaUpdate
import yokai.i18n.MR import yokai.i18n.MR
import yokai.util.lang.getString import yokai.util.lang.getString
@ -175,6 +176,12 @@ var Manga.dominantCoverColors: Pair<Int, Int>?
MangaCoverMetadata.addCoverColor(this, value.first, value.second) MangaCoverMetadata.addCoverColor(this, value.first, value.second)
} }
var Manga.vibrantCoverColor: Int?
get() = MangaCoverMetadata.getVibrantColor(id)
set(value) {
id?.let { MangaCoverMetadata.setVibrantColor(it, value) }
}
fun Manga.Companion.create(source: Long) = MangaImpl().apply { fun Manga.Companion.create(source: Long) = MangaImpl().apply {
this.source = source this.source = source
} }
@ -269,3 +276,7 @@ suspend fun Manga.updateCoverLastModified(updateManga: UpdateManga = Injekt.get(
cover_last_modified = System.currentTimeMillis() cover_last_modified = System.currentTimeMillis()
updateManga.await(MangaUpdate(id = id!!, coverLastModified = cover_last_modified)) updateManga.await(MangaUpdate(id = id!!, coverLastModified = cover_last_modified))
} }
suspend fun MangaCover.updateCoverLastModified(updateManga: UpdateManga = Injekt.get()) {
updateManga.await(MangaUpdate(id = mangaId!!, coverLastModified = System.currentTimeMillis()))
}

View file

@ -25,6 +25,7 @@ import eu.kanade.tachiyomi.util.system.getResourceColor
import eu.kanade.tachiyomi.util.view.backgroundColor import eu.kanade.tachiyomi.util.view.backgroundColor
import eu.kanade.tachiyomi.util.view.setCards import eu.kanade.tachiyomi.util.view.setCards
import eu.kanade.tachiyomi.widget.AutofitRecyclerView import eu.kanade.tachiyomi.widget.AutofitRecyclerView
import yokai.domain.manga.models.cover
/** /**
* Class used to hold the displayed data of a manga in the library, like the cover or the title. * Class used to hold the displayed data of a manga in the library, like the cover or the title.
@ -125,7 +126,7 @@ class LibraryGridHolder(
private fun setCover(manga: Manga) { private fun setCover(manga: Manga) {
if ((adapter.recyclerView.context as? Activity)?.isDestroyed == true) return if ((adapter.recyclerView.context as? Activity)?.isDestroyed == true) return
binding.coverThumbnail.loadManga(manga) { binding.coverThumbnail.loadManga(manga.cover()) {
val hasRatio = binding.coverThumbnail.layoutParams.height != ViewGroup.LayoutParams.WRAP_CONTENT val hasRatio = binding.coverThumbnail.layoutParams.height != ViewGroup.LayoutParams.WRAP_CONTENT
if (!fixedSize && !hasRatio) { if (!fixedSize && !hasRatio) {
precision(Precision.INEXACT) precision(Precision.INEXACT)

View file

@ -5,15 +5,14 @@ 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 coil3.dispose import coil3.dispose
import eu.kanade.tachiyomi.R
import yokai.i18n.MR
import yokai.util.lang.getString
import dev.icerock.moko.resources.compose.stringResource
import eu.kanade.tachiyomi.data.coil.loadManga import eu.kanade.tachiyomi.data.coil.loadManga
import eu.kanade.tachiyomi.databinding.MangaListItemBinding import eu.kanade.tachiyomi.databinding.MangaListItemBinding
import eu.kanade.tachiyomi.util.lang.highlightText import eu.kanade.tachiyomi.util.lang.highlightText
import eu.kanade.tachiyomi.util.system.dpToPx import eu.kanade.tachiyomi.util.system.dpToPx
import eu.kanade.tachiyomi.util.view.setCards import eu.kanade.tachiyomi.util.view.setCards
import yokai.domain.manga.models.cover
import yokai.i18n.MR
import yokai.util.lang.getString
/** /**
* Class used to hold the displayed data of a manga in the library, like the cover or the binding.title. * Class used to hold the displayed data of a manga in the library, like the cover or the binding.title.
@ -97,7 +96,7 @@ class LibraryListHolder(
// Update the cover. // Update the cover.
binding.coverThumbnail.dispose() binding.coverThumbnail.dispose()
binding.coverThumbnail.loadManga(item.manga) binding.coverThumbnail.loadManga(item.manga.cover())
} }
override fun onActionStateChanged(position: Int, actionState: Int) { override fun onActionStateChanged(position: Int, actionState: Int) {

View file

@ -1633,7 +1633,7 @@ class LibraryPresenter(
) { ) {
val libraryManga = getManga.awaitFavorites() val libraryManga = getManga.awaitFavorites()
libraryManga.forEach { manga -> libraryManga.forEach { manga ->
try { withUIContext { MangaCoverMetadata.setRatioAndColors(manga) } } catch (_: Exception) { } try { withUIContext { MangaCoverMetadata.setRatioAndColors(manga.id, manga.thumbnail_url, manga.favorite) } } catch (_: Exception) { }
} }
MangaCoverMetadata.savePrefs() MangaCoverMetadata.savePrefs()
} }

View file

@ -41,6 +41,7 @@ import eu.kanade.tachiyomi.widget.TachiyomiTextInputEditText
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
import yokai.domain.manga.models.cover
import yokai.i18n.MR import yokai.i18n.MR
import yokai.util.lang.getString import yokai.util.lang.getString
import android.R as AR import android.R as AR
@ -98,7 +99,7 @@ class EditMangaDialog : DialogController {
fun onViewCreated() { fun onViewCreated() {
val context = binding.root.context val context = binding.root.context
binding.mangaCover.loadManga(manga) binding.mangaCover.loadManga(manga.cover())
val isLocal = manga.isLocal() val isLocal = manga.isLocal()
binding.mangaLang.isVisible = isLocal binding.mangaLang.isVisible = isLocal

View file

@ -61,6 +61,7 @@ import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.data.database.models.Category import eu.kanade.tachiyomi.data.database.models.Category
import eu.kanade.tachiyomi.data.database.models.Chapter import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.data.database.models.seriesType import eu.kanade.tachiyomi.data.database.models.seriesType
import eu.kanade.tachiyomi.data.database.models.vibrantCoverColor
import eu.kanade.tachiyomi.data.download.DownloadJob import eu.kanade.tachiyomi.data.download.DownloadJob
import eu.kanade.tachiyomi.data.download.model.Download import eu.kanade.tachiyomi.data.download.model.Download
import eu.kanade.tachiyomi.data.notification.NotificationReceiver import eu.kanade.tachiyomi.data.notification.NotificationReceiver

View file

@ -47,6 +47,7 @@ import eu.kanade.tachiyomi.util.system.getResourceColor
import eu.kanade.tachiyomi.util.system.isInNightMode import eu.kanade.tachiyomi.util.system.isInNightMode
import eu.kanade.tachiyomi.util.system.isLTR import eu.kanade.tachiyomi.util.system.isLTR
import eu.kanade.tachiyomi.util.view.resetStrokeColor import eu.kanade.tachiyomi.util.view.resetStrokeColor
import yokai.domain.manga.models.cover
import yokai.i18n.MR import yokai.i18n.MR
import yokai.util.lang.getString import yokai.util.lang.getString
import android.R as AR import android.R as AR
@ -671,7 +672,7 @@ class MangaHeaderHolder(
if (!manga.initialized) return if (!manga.initialized) return
val drawable = adapter.controller.binding.mangaCoverFull.drawable val drawable = adapter.controller.binding.mangaCoverFull.drawable
binding.mangaCover.loadManga( binding.mangaCover.loadManga(
manga, manga.cover(),
builder = { builder = {
placeholder(drawable) placeholder(drawable)
error(drawable) error(drawable)
@ -680,7 +681,7 @@ class MangaHeaderHolder(
}, },
) )
binding.backdrop.loadManga( binding.backdrop.loadManga(
manga, manga.cover(),
builder = { builder = {
placeholder(drawable) placeholder(drawable)
error(drawable) error(drawable)

View file

@ -9,6 +9,7 @@ import eu.kanade.tachiyomi.data.coil.loadManga
import eu.kanade.tachiyomi.databinding.MangaListItemBinding import eu.kanade.tachiyomi.databinding.MangaListItemBinding
import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder
import eu.kanade.tachiyomi.util.view.setCards import eu.kanade.tachiyomi.util.view.setCards
import yokai.domain.manga.models.cover
class MangaHolder( class MangaHolder(
view: View, view: View,
@ -29,6 +30,6 @@ class MangaHolder(
// Update the cover. // Update the cover.
binding.coverThumbnail.dispose() binding.coverThumbnail.dispose()
binding.coverThumbnail.loadManga(item.manga) binding.coverThumbnail.loadManga(item.manga.cover())
} }
} }

View file

@ -16,9 +16,6 @@ import androidx.core.view.updatePaddingRelative
import androidx.transition.TransitionManager import androidx.transition.TransitionManager
import androidx.transition.TransitionSet import androidx.transition.TransitionSet
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import yokai.i18n.MR
import yokai.util.lang.getString
import dev.icerock.moko.resources.compose.stringResource
import eu.kanade.tachiyomi.data.coil.loadManga import eu.kanade.tachiyomi.data.coil.loadManga
import eu.kanade.tachiyomi.data.database.models.Chapter import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.data.database.models.ChapterHistory import eu.kanade.tachiyomi.data.database.models.ChapterHistory
@ -37,6 +34,9 @@ import eu.kanade.tachiyomi.util.view.setAnimVectorCompat
import eu.kanade.tachiyomi.util.view.setCards import eu.kanade.tachiyomi.util.view.setCards
import java.util.* import java.util.*
import java.util.concurrent.* import java.util.concurrent.*
import yokai.domain.manga.models.cover
import yokai.i18n.MR
import yokai.util.lang.getString
import android.R as AR import android.R as AR
class RecentMangaHolder( class RecentMangaHolder(
@ -213,7 +213,7 @@ class RecentMangaHolder(
else -> context.timeSpanFromNow(MR.strings.read_, item.mch.history.last_read) else -> context.timeSpanFromNow(MR.strings.read_, item.mch.history.last_read)
} }
if ((context as? Activity)?.isDestroyed != true) { if ((context as? Activity)?.isDestroyed != true) {
binding.coverThumbnail.loadManga(item.mch.manga) binding.coverThumbnail.loadManga(item.mch.manga.cover())
} }
if (!item.mch.manga.isLocal()) { if (!item.mch.manga.isLocal()) {
notifyStatus( notifyStatus(

View file

@ -6,7 +6,6 @@ import androidx.palette.graphics.Palette
import com.hippo.unifile.UniFile import com.hippo.unifile.UniFile
import eu.kanade.tachiyomi.data.cache.CoverCache import eu.kanade.tachiyomi.data.cache.CoverCache
import eu.kanade.tachiyomi.data.coil.getBestColor import eu.kanade.tachiyomi.data.coil.getBestColor
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 java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentHashMap
@ -16,6 +15,7 @@ import uy.kohesive.injekt.injectLazy
object MangaCoverMetadata { 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>>()
private var vibrantCoverColorMap = ConcurrentHashMap<Long, Int>()
private val preferences by injectLazy<PreferencesHelper>() private val preferences by injectLazy<PreferencesHelper>()
private val coverCache by injectLazy<CoverCache>() private val coverCache by injectLazy<CoverCache>()
@ -49,19 +49,19 @@ object MangaCoverMetadata {
) )
} }
fun setRatioAndColors(manga: Manga, ogFile: UniFile? = null, force: Boolean = false) { fun setRatioAndColors(mangaId: Long?, mangaThumbnailUrl: String?, isInLibrary: Boolean, ogFile: UniFile? = null, force: Boolean = false) {
if (!manga.favorite) { if (!isInLibrary) {
remove(manga) remove(mangaId)
} }
if (manga.vibrantCoverColor != null && !manga.favorite) return if (getVibrantColor(mangaId) != null && !isInLibrary) return
val file = ogFile val file = ogFile
?: UniFile.fromFile(coverCache.getCustomCoverFile(manga))?.takeIf { it.exists() } ?: UniFile.fromFile(coverCache.getCustomCoverFile(mangaId))?.takeIf { it.exists() }
?: UniFile.fromFile(coverCache.getCoverFile(manga.thumbnail_url, !manga.favorite)) ?: UniFile.fromFile(coverCache.getCoverFile(mangaThumbnailUrl, !isInLibrary))
// 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() == true) { if (file?.exists() == true) {
val options = BitmapFactory.Options() val options = BitmapFactory.Options()
val hasVibrantColor = if (manga.favorite) manga.vibrantCoverColor != null else true val hasVibrantColor = if (isInLibrary) vibrantCoverColorMap[mangaId] != null else true
if (manga.dominantCoverColors != null && hasVibrantColor && !force) { if (getColors(mangaId) != null && hasVibrantColor && !force) {
options.inJustDecodeBounds = true options.inJustDecodeBounds = true
} else { } else {
options.inSampleSize = 4 options.inSampleSize = 4
@ -70,45 +70,74 @@ object MangaCoverMetadata {
if (bitmap != null) { if (bitmap != null) {
Palette.from(bitmap).generate { Palette.from(bitmap).generate {
if (it == null) return@generate if (it == null) return@generate
if (manga.favorite) { if (isInLibrary) {
it.dominantSwatch?.let { swatch -> it.dominantSwatch?.let { swatch ->
manga.dominantCoverColors = swatch.rgb to swatch.titleTextColor addCoverColor(mangaId, swatch.rgb, swatch.titleTextColor)
} }
} }
val color = it.getBestColor() ?: return@generate val color = it.getBestColor() ?: return@generate
manga.vibrantCoverColor = color setVibrantColor(mangaId, color)
} }
} }
if (manga.favorite && !(options.outWidth == -1 || options.outHeight == -1)) { if (isInLibrary && !(options.outWidth == -1 || options.outHeight == -1)) {
addCoverRatio(manga, options.outWidth / options.outHeight.toFloat()) addCoverRatio(mangaId, options.outWidth / options.outHeight.toFloat())
} }
} }
} }
fun remove(manga: Manga) { fun remove(manga: Manga) {
val id = manga.id ?: return remove(manga.id)
coverRatioMap.remove(id) }
coverColorMap.remove(id)
fun remove(mangaId: Long?) {
mangaId ?: return
coverRatioMap.remove(mangaId)
coverColorMap.remove(mangaId)
} }
fun addCoverRatio(manga: Manga, ratio: Float) { fun addCoverRatio(manga: Manga, ratio: Float) {
val id = manga.id ?: return addCoverRatio(manga.id, ratio)
coverRatioMap[id] = ratio }
fun addCoverRatio(mangaId: Long?, ratio: Float) {
mangaId ?: return
coverRatioMap[mangaId] = ratio
} }
fun addCoverColor(manga: Manga, @ColorInt color: Int, @ColorInt textColor: Int) { fun addCoverColor(manga: Manga, @ColorInt color: Int, @ColorInt textColor: Int) {
val id = manga.id ?: return addCoverColor(manga.id, color, textColor)
coverColorMap[id] = color to textColor
} }
fun getColors(manga: Manga): Pair<Int, Int>? { fun addCoverColor(mangaId: Long?, @ColorInt color: Int, @ColorInt textColor: Int) {
return coverColorMap[manga.id] mangaId ?: return
coverColorMap[mangaId] = color to textColor
}
fun getColors(manga: Manga): Pair<Int, Int>? = getColors(manga.id)
fun getColors(mangaId: Long?): Pair<Int, Int>? {
return coverColorMap[mangaId]
} }
fun getRatio(manga: Manga): Float? { fun getRatio(manga: Manga): Float? {
return coverRatioMap[manga.id] return coverRatioMap[manga.id]
} }
fun setVibrantColor(mangaId: Long?, @ColorInt color: Int?) {
mangaId ?: return
if (color == null) {
vibrantCoverColorMap.remove(mangaId)
return
}
vibrantCoverColorMap[mangaId] = color
}
fun getVibrantColor(mangaId: Long?): Int? {
return vibrantCoverColorMap[mangaId]
}
fun savePrefs() { fun savePrefs() {
val mapCopy = coverRatioMap.toMap() val mapCopy = coverRatioMap.toMap()
preferences.coverRatios().set(mapCopy.map { "${it.key}|${it.value}" }.toSet()) preferences.coverRatios().set(mapCopy.map { "${it.key}|${it.value}" }.toSet())

View file

@ -2,7 +2,6 @@ package eu.kanade.tachiyomi.domain.manga.models
import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.model.SManga
import java.util.Locale import java.util.Locale
import kotlin.collections.set
import yokai.domain.manga.models.MangaUpdate import yokai.domain.manga.models.MangaUpdate
// TODO: Transform into data class // TODO: Transform into data class
@ -197,12 +196,6 @@ interface Manga : SManga {
get() = chapter_flags and CHAPTER_SORTING_MASK get() = chapter_flags and CHAPTER_SORTING_MASK
set(sort) = setChapterFlags(sort, CHAPTER_SORTING_MASK) set(sort) = setChapterFlags(sort, CHAPTER_SORTING_MASK)
var vibrantCoverColor: Int?
get() = vibrantCoverColorMap[id]
set(value) {
id?.let { vibrantCoverColorMap[it] = value }
}
fun toMangaUpdate(): MangaUpdate { fun toMangaUpdate(): MangaUpdate {
return MangaUpdate( return MangaUpdate(
id = id!!, id = id!!,
@ -268,7 +261,5 @@ interface Manga : SManga {
const val TYPE_MANHUA = 3 const val TYPE_MANHUA = 3
const val TYPE_COMIC = 4 const val TYPE_COMIC = 4
const val TYPE_WEBTOON = 5 const val TYPE_WEBTOON = 5
private val vibrantCoverColorMap: HashMap<Long, Int?> = hashMapOf()
} }
} }

View file

@ -23,4 +23,5 @@ data class Manga(
var chapterFlags: Int, var chapterFlags: Int,
var hideTitle: Boolean, var hideTitle: Boolean,
var filteredScanlators: String?, var filteredScanlators: String?,
var coverLastModified: Long,
): Serializable ): Serializable

View file

@ -0,0 +1,27 @@
package yokai.domain.manga.models
import eu.kanade.tachiyomi.domain.manga.models.Manga as TachiManga
data class MangaCover(
val mangaId: Long?,
val sourceId: Long,
val url: String,
val lastModified: Long,
val inLibrary: Boolean,
)
fun TachiManga.cover() = MangaCover(
mangaId = id,
sourceId = source,
url = thumbnail_url ?: "",
lastModified = cover_last_modified,
inLibrary = favorite,
)
fun Manga.cover() = MangaCover(
mangaId = id,
sourceId = source,
url = thumbnailUrl ?: "",
lastModified = coverLastModified,
inLibrary = favorite,
)