Switch to coil from Glide (#423)

* initial coil switch

* more coil changes

* fix extensions icons

* remove last of glide

* adjust local manga to actually update the covers as soon as you set it.  Also adjusts the large cover and share

* edit custom covers of a manga is now immediately reflected

* fix edit covers submit not automatically submitting
fix edit covers choosing cover not showing the selection in dialog

* fix setting custom cover not reloading when going back

* get gif's working

* run ktlint
fix setting custom cover to updated when returning back to details

* fix non uniformed covers

* get images working on resumes

* add size to cover cache setting

* remove log statement

* remove set last cover date

* put covers into cache when refresh enabled

* fix comment
This commit is contained in:
Carlos 2020-05-16 23:35:16 -04:00 committed by GitHub
parent d99f4d1fac
commit 4d860c9396
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
45 changed files with 626 additions and 912 deletions

View File

@ -76,8 +76,8 @@ android {
} }
compileOptions { compileOptions {
setSourceCompatibility(1.8) sourceCompatibility = JavaVersion.VERSION_1_8
setTargetCompatibility(1.8) targetCompatibility = JavaVersion.VERSION_1_8
} }
kotlinOptions { kotlinOptions {
jvmTarget = "1.8" jvmTarget = "1.8"
@ -141,6 +141,21 @@ dependencies {
debugImplementation ("com.github.ChuckerTeam.Chucker:library:$chuckerVersion") debugImplementation ("com.github.ChuckerTeam.Chucker:library:$chuckerVersion")
releaseImplementation ("com.github.ChuckerTeam.Chucker:library-no-op:$chuckerVersion") releaseImplementation ("com.github.ChuckerTeam.Chucker:library-no-op:$chuckerVersion")
//hyperion
val hyperionVersion = "0.9.27"
debugImplementation("com.willowtreeapps.hyperion:hyperion-core:$hyperionVersion")
debugImplementation("com.willowtreeapps.hyperion:hyperion-timber:$hyperionVersion")
debugImplementation("com.willowtreeapps.hyperion:hyperion-core:$hyperionVersion")
debugImplementation("com.willowtreeapps.hyperion:hyperion-attr:$hyperionVersion")
debugImplementation("com.willowtreeapps.hyperion:hyperion-build-config:$hyperionVersion")
debugImplementation("com.willowtreeapps.hyperion:hyperion-crash:$hyperionVersion")
debugImplementation("com.willowtreeapps.hyperion:hyperion-disk:$hyperionVersion")
debugImplementation("com.willowtreeapps.hyperion:hyperion-geiger-counter:$hyperionVersion")
debugImplementation("com.willowtreeapps.hyperion:hyperion-measurement:$hyperionVersion")
debugImplementation("com.willowtreeapps.hyperion:hyperion-phoenix:$hyperionVersion")
debugImplementation("com.willowtreeapps.hyperion:hyperion-recorder:$hyperionVersion")
debugImplementation("com.willowtreeapps.hyperion:hyperion-shared-preferences:$hyperionVersion")
// REST // REST
val retrofitVersion = "2.7.2" val retrofitVersion = "2.7.2"
implementation("com.squareup.retrofit2:retrofit:$retrofitVersion") implementation("com.squareup.retrofit2:retrofit:$retrofitVersion")
@ -184,10 +199,10 @@ dependencies {
implementation("com.github.inorichi.injekt:injekt-core:65b0440") implementation("com.github.inorichi.injekt:injekt-core:65b0440")
// Image library // Image library
val glideVersion = "4.11.0" val coilVersion = "0.10.1"
implementation("com.github.bumptech.glide:glide:$glideVersion") implementation("io.coil-kt:coil:$coilVersion")
implementation("com.github.bumptech.glide:okhttp3-integration:$glideVersion") implementation("io.coil-kt:coil-gif:$coilVersion")
kapt("com.github.bumptech.glide:compiler:$glideVersion") implementation("io.coil-kt:coil-svg:$coilVersion")
// Logging // Logging
implementation("com.jakewharton.timber:timber:4.7.1") implementation("com.jakewharton.timber:timber:4.7.1")

View File

@ -27,14 +27,6 @@
-dontwarn javax.annotation.** -dontwarn javax.annotation.**
-dontwarn retrofit2.Platform$Java8 -dontwarn retrofit2.Platform$Java8
# Glide specific rules #
# https://github.com/bumptech/glide
-keep public class * implements com.bumptech.glide.module.GlideModule
-keep public class * extends com.bumptech.glide.AppGlideModule
-keep public enum com.bumptech.glide.load.resource.bitmap.ImageHeaderParser$** {
**[] $VALUES;
public *;
}
# RxJava 1.1.0 # RxJava 1.1.0
-dontwarn sun.misc.** -dontwarn sun.misc.**

View File

@ -8,6 +8,7 @@ import androidx.lifecycle.LifecycleObserver
import androidx.lifecycle.OnLifecycleEvent import androidx.lifecycle.OnLifecycleEvent
import androidx.lifecycle.ProcessLifecycleOwner import androidx.lifecycle.ProcessLifecycleOwner
import androidx.multidex.MultiDex import androidx.multidex.MultiDex
import eu.kanade.tachiyomi.data.download.coil.CoilSetup
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
import eu.kanade.tachiyomi.data.preference.getOrDefault import eu.kanade.tachiyomi.data.preference.getOrDefault
@ -18,6 +19,7 @@ import org.acra.annotation.ReportsCrashes
import timber.log.Timber import timber.log.Timber
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.InjektScope import uy.kohesive.injekt.api.InjektScope
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
import uy.kohesive.injekt.registry.default.DefaultRegistrar import uy.kohesive.injekt.registry.default.DefaultRegistrar
@ -37,6 +39,7 @@ open class App : Application(), LifecycleObserver {
Injekt = InjektScope(DefaultRegistrar()) Injekt = InjektScope(DefaultRegistrar())
Injekt.importModule(AppModule(this)) Injekt.importModule(AppModule(this))
CoilSetup(this)
setupAcra() setupAcra()
setupNotificationChannels() setupNotificationChannels()

View File

@ -2,8 +2,10 @@ package eu.kanade.tachiyomi.data.cache
import android.content.Context import android.content.Context
import android.text.format.Formatter import android.text.format.Formatter
import coil.Coil
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.util.storage.DiskUtil import eu.kanade.tachiyomi.util.storage.DiskUtil
import eu.kanade.tachiyomi.util.system.executeOnIO import eu.kanade.tachiyomi.util.system.executeOnIO
import eu.kanade.tachiyomi.util.system.toast import eu.kanade.tachiyomi.util.system.toast
@ -11,6 +13,7 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import okhttp3.Cache
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import java.io.File import java.io.File
@ -20,13 +23,12 @@ import java.io.InputStream
/** /**
* Class used to create cover cache. * Class used to create cover cache.
* It is used to store the covers of the library. * It is used to store the covers of the library.
* Makes use of Glide (which can avoid repeating requests) to download covers.
* Names of files are created with the md5 of the thumbnail URL. * Names of files are created with the md5 of the thumbnail URL.
* *
* @param context the application context. * @param context the application context.
* @constructor creates an instance of the cover cache. * @constructor creates an instance of the cover cache.
*/ */
class CoverCache(private val context: Context) { class CoverCache(val context: Context) {
/** /**
* Cache directory used for cache management. * Cache directory used for cache management.
@ -34,14 +36,18 @@ class CoverCache(private val context: Context) {
private val cacheDir = context.getExternalFilesDir("covers") private val cacheDir = context.getExternalFilesDir("covers")
?: File(context.filesDir, "covers").also { it.mkdirs() } ?: File(context.filesDir, "covers").also { it.mkdirs() }
val cache = Cache(cacheDir, 300 * 1024 * 1024) // 300MB
fun getChapterCacheSize(): String {
return Formatter.formatFileSize(context, DiskUtil.getDirectorySize(cacheDir))
}
fun deleteOldCovers() { fun deleteOldCovers() {
GlobalScope.launch(Dispatchers.Default) { GlobalScope.launch(Dispatchers.Default) {
val db = Injekt.get<DatabaseHelper>() val db = Injekt.get<DatabaseHelper>()
var deletedSize = 0L var deletedSize = 0L
val urls = db.getLibraryMangas().executeOnIO().mapNotNull { val urls = db.getLibraryMangas().executeOnIO().mapNotNull {
it.thumbnail_url?.let { url -> it.thumbnail_url?.let { url -> return@mapNotNull it.key() }
return@mapNotNull DiskUtil.hashKeyForDisk(url)
}
null null
} }
val files = cacheDir.listFiles()?.iterator() ?: return@launch val files = cacheDir.listFiles()?.iterator() ?: return@launch
@ -68,8 +74,8 @@ class CoverCache(private val context: Context) {
* @param thumbnailUrl the thumbnail url. * @param thumbnailUrl the thumbnail url.
* @return cover image. * @return cover image.
*/ */
fun getCoverFile(thumbnailUrl: String): File { fun getCoverFile(manga: Manga): File {
return File(cacheDir, DiskUtil.hashKeyForDisk(thumbnailUrl)) return File(cacheDir, manga.key())
} }
/** /**
@ -80,26 +86,26 @@ class CoverCache(private val context: Context) {
* @throws IOException if there's any error. * @throws IOException if there's any error.
*/ */
@Throws(IOException::class) @Throws(IOException::class)
fun copyToCache(thumbnailUrl: String, inputStream: InputStream) { fun copyToCache(manga: Manga, inputStream: InputStream) {
// Get destination file. // Get destination file.
val destFile = getCoverFile(thumbnailUrl) val destFile = getCoverFile(manga)
destFile.outputStream().use { inputStream.copyTo(it) } destFile.outputStream().use { inputStream.copyTo(it) }
} }
/** /**
* Delete the cover file from the cache. * Delete the cover file from the disk cache and optional from memory cache
* *
* @param thumbnailUrl the thumbnail url. * @param thumbnailUrl the thumbnail url.
* @return status of deletion. * @return status of deletion.
*/ */
fun deleteFromCache(thumbnailUrl: String?): Boolean { fun deleteFromCache(manga: Manga, deleteMemoryCache: Boolean = true) {
// Check if url is empty. // Check if url is empty.
if (thumbnailUrl.isNullOrEmpty()) if (manga.thumbnail_url.isNullOrEmpty()) return
return false
// Remove file. // Remove file
val file = getCoverFile(thumbnailUrl) val file = getCoverFile(manga)
return file.exists() && file.delete() if (deleteMemoryCache) Coil.imageLoader(context).invalidate(file.name)
if (file.exists()) file.delete()
} }
} }

View File

@ -5,9 +5,11 @@ import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.source.SourceManager import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.ui.reader.ReaderActivity import eu.kanade.tachiyomi.ui.reader.ReaderActivity
import eu.kanade.tachiyomi.util.storage.DiskUtil
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import java.util.Locale import java.util.Locale
import kotlin.random.Random
interface Manga : SManga { interface Manga : SManga {
@ -52,13 +54,15 @@ interface Manga : SManga {
fun showChapterTitle(defaultShow: Boolean): Boolean = chapter_flags and DISPLAY_MASK == DISPLAY_NUMBER fun showChapterTitle(defaultShow: Boolean): Boolean = chapter_flags and DISPLAY_MASK == DISPLAY_NUMBER
fun mangaType(context: Context): String { fun mangaType(context: Context): String {
return context.getString(when (mangaType()) { return context.getString(
when (mangaType()) {
TYPE_WEBTOON -> R.string.webtoon TYPE_WEBTOON -> R.string.webtoon
TYPE_MANHWA -> R.string.manhwa TYPE_MANHWA -> R.string.manhwa
TYPE_MANHUA -> R.string.manhua TYPE_MANHUA -> R.string.manhua
TYPE_COMIC -> R.string.comic TYPE_COMIC -> R.string.comic
else -> R.string.manga else -> R.string.manga
}).toLowerCase(Locale.getDefault()) }
).toLowerCase(Locale.getDefault())
} }
/** /**
@ -97,14 +101,16 @@ interface Manga : SManga {
return if (currentTags?.any return if (currentTags?.any
{ tag -> { tag ->
tag == "long strip" || tag == "manhwa" || tag.contains("webtoon") tag == "long strip" || tag == "manhwa" || tag.contains("webtoon")
} == true || isWebtoonSource(sourceName)) } == true || isWebtoonSource(sourceName)
)
ReaderActivity.WEBTOON ReaderActivity.WEBTOON
else if (currentTags?.any else if (currentTags?.any
{ tag -> { tag ->
tag == "chinese" || tag == "manhua" || tag == "chinese" || tag == "manhua" ||
tag.startsWith("english") || tag == "comic" tag.startsWith("english") || tag == "comic"
} == true || (isComicSource(sourceName) && !sourceName.contains("tapastic", true)) || } == true || (isComicSource(sourceName) && !sourceName.contains("tapastic", true)) ||
sourceName.contains("manhua", true)) sourceName.contains("manhua", true)
)
ReaderActivity.LEFT_TO_RIGHT ReaderActivity.LEFT_TO_RIGHT
else 0 else 0
} }
@ -139,6 +145,19 @@ interface Manga : SManga {
sourceName.contains("tapastic", true) sourceName.contains("tapastic", true)
} }
fun key(): String {
return DiskUtil.hashKeyForDisk(thumbnail_url.orEmpty())
}
fun setCustomThumbnailUrl() {
removeCustomThumbnailUrl()
thumbnail_url = "Custom-${Random.nextInt(0, 1000)}-J2K-${thumbnail_url ?: id!!}"
}
fun removeCustomThumbnailUrl() {
thumbnail_url = thumbnail_url?.substringAfter("-J2K-")?.substringAfter("Custom-")
}
// Used to display the chapter's title one way or another // Used to display the chapter's title one way or another
var displayMode: Int var displayMode: Int
get() = chapter_flags and DISPLAY_MASK get() = chapter_flags and DISPLAY_MASK

View File

@ -67,14 +67,4 @@ open class MangaImpl : Manga {
if (::url.isInitialized) return url.hashCode() if (::url.isInitialized) return url.hashCode()
else return (id ?: 0L).hashCode() else return (id ?: 0L).hashCode()
} }
companion object {
private var lastCoverFetch: HashMap<Long, Long> = hashMapOf()
fun setLastCoverFetch(id: Long, time: Long) {
lastCoverFetch[id] = time
}
fun getLastCoverFetch(id: Long) = lastCoverFetch[id] ?: 0
}
} }

View File

@ -0,0 +1,31 @@
package eu.kanade.tachiyomi.data.download.coil
import coil.bitmappool.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 {
val source = ByteArrayInputStream(data).source().buffer()
return SourceResult(
source = ByteArrayInputStream(data).source().buffer(),
mimeType = "image/gif",
dataSource = DataSource.MEMORY
)
}
}

View File

@ -0,0 +1,42 @@
package eu.kanade.tachiyomi.data.download.coil
import android.content.Context
import android.os.Build
import coil.Coil
import coil.ImageLoader
import coil.decode.GifDecoder
import coil.decode.ImageDecoderDecoder
import coil.decode.SvgDecoder
import coil.util.CoilUtils
import com.chuckerteam.chucker.api.ChuckerInterceptor
import eu.kanade.tachiyomi.R
import okhttp3.OkHttpClient
class CoilSetup(context: Context) {
init {
val imageLoader = ImageLoader.Builder(context)
.availableMemoryPercentage(0.40)
.crossfade(true)
.allowRgb565(true)
.allowHardware(false)
.error(R.drawable.ic_broken_image_grey_24dp)
.componentRegistry {
if (Build.VERSION.SDK_INT >= 28) {
add(ImageDecoderDecoder())
} else {
add(GifDecoder())
}
add(SvgDecoder(context))
add(MangaFetcher())
add(ByteArrayFetcher())
}.okHttpClient {
OkHttpClient.Builder()
.cache(CoilUtils.createDefaultCache(context))
.addInterceptor(ChuckerInterceptor(context))
.build()
}
.build()
Coil.setImageLoader(imageLoader)
}
}

View File

@ -0,0 +1,125 @@
package eu.kanade.tachiyomi.data.download.coil
import coil.bitmappool.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.source.SourceManager
import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.util.storage.DiskUtil
import okhttp3.Call
import okhttp3.Request
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
class MangaFetcher() : Fetcher<Manga> {
private val coverCache: CoverCache by injectLazy()
private val sourceManager: SourceManager by injectLazy()
private val defaultClient = Injekt.get<NetworkHelper>().client
override fun key(manga: Manga): String? {
if (manga.thumbnail_url.isNullOrBlank()) return null
return DiskUtil.hashKeyForDisk(manga.thumbnail_url!!)
}
override suspend fun fetch(pool: BitmapPool, manga: Manga, size: Size, options: Options): FetchResult {
val cover = manga.thumbnail_url
when (getResourceType(cover)) {
Type.File -> {
return fileLoader(manga)
}
Type.URL -> {
return httpLoader(manga)
}
Type.CUSTOM -> {
return customLoader(manga)
}
null -> error("Invalid image")
}
}
private fun customLoader(manga: Manga): FetchResult {
val coverFile = coverCache.getCoverFile(manga)
if (coverFile.exists()) {
return fileLoader(coverFile)
}
manga.thumbnail_url = manga.thumbnail_url!!.substringAfter("-J2K-").substringAfter("CUSTOM-")
return httpLoader(manga)
}
private fun httpLoader(manga: Manga): FetchResult {
val coverFile = coverCache.getCoverFile(manga)
if (coverFile.exists()) {
return fileLoader(coverFile)
}
val call = getCall(manga)
val tmpFile = File(coverFile.absolutePath + "_tmp")
val response = call.execute()
val body = checkNotNull(response.body) { "Null response source" }
body.source().use { input ->
tmpFile.sink().buffer().use { output ->
output.writeAll(input)
}
}
tmpFile.renameTo(coverFile)
return fileLoader(coverFile)
}
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 getCall(manga: Manga): Call {
val source = sourceManager.get(manga.source) as? HttpSource
val client = source?.client ?: defaultClient
val newClient = client.newBuilder()
.cache(coverCache.cache)
.build()
val request = Request.Builder().url(manga.thumbnail_url!!).also {
if (source != null) {
it.headers(source.headers)
}
}.build()
return newClient.newCall(request)
}
private fun getResourceType(cover: String?): Type? {
return when {
cover.isNullOrEmpty() -> null
cover.startsWith("http") -> Type.URL
cover.startsWith("Custom-") -> Type.CUSTOM
cover.startsWith("/") || cover.startsWith("file://") -> Type.File
else -> null
}
}
private enum class Type {
File, CUSTOM, URL;
}
}

View File

@ -1,55 +0,0 @@
package eu.kanade.tachiyomi.data.glide
import android.content.ContentValues.TAG
import android.util.Log
import com.bumptech.glide.Priority
import com.bumptech.glide.load.DataSource
import com.bumptech.glide.load.data.DataFetcher
import java.io.File
import java.io.FileInputStream
import java.io.FileNotFoundException
import java.io.IOException
import java.io.InputStream
open class FileFetcher(private val file: File) : DataFetcher<InputStream> {
private var data: InputStream? = null
override fun loadData(priority: Priority, callback: DataFetcher.DataCallback<in InputStream>) {
loadFromFile(callback)
}
protected fun loadFromFile(callback: DataFetcher.DataCallback<in InputStream>) {
try {
data = FileInputStream(file)
} catch (e: FileNotFoundException) {
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "Failed to open file", e)
}
callback.onLoadFailed(e)
return
}
callback.onDataReady(data)
}
override fun cleanup() {
try {
data?.close()
} catch (e: IOException) {
// Ignored.
}
}
override fun cancel() {
// Do nothing.
}
override fun getDataClass(): Class<InputStream> {
return InputStream::class.java
}
override fun getDataSource(): DataSource {
return DataSource.LOCAL
}
}

View File

@ -1,72 +0,0 @@
package eu.kanade.tachiyomi.data.glide
import com.bumptech.glide.Priority
import com.bumptech.glide.load.data.DataFetcher
import eu.kanade.tachiyomi.data.database.models.Manga
import java.io.File
import java.io.FileNotFoundException
import java.io.InputStream
/**
* A [DataFetcher] for loading a cover of a library manga.
* It tries to load the cover from our custom cache, and if it's not found, it fallbacks to network
* and copies the result to the cache.
*
* @param networkFetcher the network fetcher for this cover.
* @param manga the manga of the cover to load.
* @param file the file where this cover should be. It may exists or not.
*/
class LibraryMangaUrlFetcher(
private val networkFetcher: DataFetcher<InputStream>,
private val manga: Manga,
private val file: File
) :
FileFetcher(file) {
override fun loadData(priority: Priority, callback: DataFetcher.DataCallback<in InputStream>) {
if (!file.exists()) {
networkFetcher.loadData(priority, object : DataFetcher.DataCallback<InputStream> {
override fun onDataReady(data: InputStream?) {
if (data != null) {
val tmpFile = File(file.path + ".tmp")
try {
// Retrieve destination stream, create parent folders if needed.
val output = try {
tmpFile.outputStream()
} catch (e: FileNotFoundException) {
tmpFile.parentFile.mkdirs()
tmpFile.outputStream()
}
// Copy the file and rename to the original.
data.use { output.use { data.copyTo(output) } }
tmpFile.renameTo(file)
loadFromFile(callback)
} catch (e: Exception) {
tmpFile.delete()
callback.onLoadFailed(e)
}
} else {
callback.onLoadFailed(Exception("Null data"))
}
}
override fun onLoadFailed(e: Exception) {
callback.onLoadFailed(e)
}
})
} else {
loadFromFile(callback)
}
}
override fun cleanup() {
super.cleanup()
networkFetcher.cleanup()
}
override fun cancel() {
super.cancel()
networkFetcher.cancel()
}
}

View File

@ -1,156 +0,0 @@
package eu.kanade.tachiyomi.data.glide
import android.util.LruCache
import com.bumptech.glide.integration.okhttp3.OkHttpStreamFetcher
import com.bumptech.glide.load.Options
import com.bumptech.glide.load.model.GlideUrl
import com.bumptech.glide.load.model.Headers
import com.bumptech.glide.load.model.LazyHeaders
import com.bumptech.glide.load.model.ModelLoader
import com.bumptech.glide.load.model.ModelLoaderFactory
import com.bumptech.glide.load.model.MultiModelLoaderFactory
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.source.SourceManager
import eu.kanade.tachiyomi.source.online.HttpSource
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
import java.io.File
import java.io.InputStream
/**
* A class for loading a cover associated with a [Manga] that can be present in our own cache.
* Coupled with [LibraryMangaUrlFetcher], this class allows to implement the following flow:
*
* - Check in RAM LRU.
* - Check in disk LRU.
* - Check in this module.
* - Fetch from the network connection.
*
* @param context the application context.
*/
class MangaModelLoader : ModelLoader<Manga, InputStream> {
/**
* Cover cache where persistent covers are stored.
*/
private val coverCache: CoverCache by injectLazy()
/**
* Source manager.
*/
private val sourceManager: SourceManager by injectLazy()
/**
* Default network client.
*/
private val defaultClient = Injekt.get<NetworkHelper>().client
/**
* LRU cache whose key is the thumbnail url of the manga, and the value contains the request url
* and the file where it should be stored in case the manga is a favorite.
*/
private val lruCache = LruCache<GlideUrl, File>(100)
/**
* Map where request headers are stored for a source.
*/
private val cachedHeaders = hashMapOf<Long, LazyHeaders>()
/**
* Factory class for creating [MangaModelLoader] instances.
*/
class Factory : ModelLoaderFactory<Manga, InputStream> {
override fun build(multiFactory: MultiModelLoaderFactory): ModelLoader<Manga, InputStream> {
return MangaModelLoader()
}
override fun teardown() {}
}
override fun handles(model: Manga): Boolean {
return true
}
/**
* Returns a fetcher for the given manga or null if the url is empty.
*
* @param manga the model.
* @param width the width of the view where the resource will be loaded.
* @param height the height of the view where the resource will be loaded.
*/
override fun buildLoadData(
manga: Manga,
width: Int,
height: Int,
options: Options
): ModelLoader.LoadData<InputStream>? {
// Check thumbnail is not null or empty
val url = manga.thumbnail_url
if (url?.startsWith("http") == true) {
val source = sourceManager.get(manga.source) as? HttpSource
val glideUrl = GlideUrl(url, getHeaders(manga, source))
// Get the resource fetcher for this request url.
val networkFetcher = OkHttpStreamFetcher(source?.client ?: defaultClient, glideUrl)
if (!manga.favorite) {
return ModelLoader.LoadData(glideUrl, networkFetcher)
}
// Obtain the file for this url from the LRU cache, or retrieve and add it to the cache.
val file = lruCache.getOrPut(glideUrl) { coverCache.getCoverFile(url) }
val libraryFetcher = LibraryMangaUrlFetcher(networkFetcher, manga, file)
// Return an instance of the fetcher providing the needed elements.
return ModelLoader.LoadData(MangaSignature(manga, file), libraryFetcher)
} else {
// Get the file from the url, removing the scheme if present, or from the cache if no url.
val file = when {
manga.hasCustomCover() -> coverCache.getCoverFile(manga.thumbnail_url!!)
url != null -> File(url.substringAfter("file://"))
else -> null
}
if (file?.exists() != true) return null
// Return an instance of the fetcher providing the needed elements.
return ModelLoader.LoadData(MangaSignature(manga, file), FileFetcher(file))
}
}
/**
* Returns the request headers for a source copying its OkHttp headers and caching them.
*
* @param manga the model.
*/
fun getHeaders(manga: Manga, source: HttpSource?): Headers {
if (source == null) return LazyHeaders.DEFAULT
return cachedHeaders.getOrPut(manga.source) {
LazyHeaders.Builder().apply {
val nullStr: String? = null
setHeader("User-Agent", nullStr)
for ((key, value) in source.headers.toMultimap()) {
addHeader(key, value[0])
}
}.build()
}
}
private inline fun <K, V> LruCache<K, V>.getOrPut(key: K, defaultValue: () -> V): V {
val value = get(key)
return if (value == null) {
val answer = defaultValue()
put(key, answer)
answer
} else {
value
}
}
}

View File

@ -1,27 +0,0 @@
package eu.kanade.tachiyomi.data.glide
import com.bumptech.glide.load.Key
import eu.kanade.tachiyomi.data.database.models.Manga
import java.io.File
import java.security.MessageDigest
class MangaSignature(manga: Manga, file: File) : Key {
private val key = manga.thumbnail_url + file.lastModified()
override fun equals(other: Any?): Boolean {
return if (other is MangaSignature) {
key == other.key
} else {
false
}
}
override fun hashCode(): Int {
return key.hashCode()
}
override fun updateDiskCacheKey(md: MessageDigest) {
md.update(key.toByteArray(Key.CHARSET))
}
}

View File

@ -1,72 +0,0 @@
package eu.kanade.tachiyomi.data.glide
import com.bumptech.glide.Priority
import com.bumptech.glide.load.DataSource
import com.bumptech.glide.load.Options
import com.bumptech.glide.load.data.DataFetcher
import com.bumptech.glide.load.model.ModelLoader
import com.bumptech.glide.load.model.ModelLoaderFactory
import com.bumptech.glide.load.model.MultiModelLoaderFactory
import com.bumptech.glide.signature.ObjectKey
import java.io.IOException
import java.io.InputStream
class PassthroughModelLoader : ModelLoader<InputStream, InputStream> {
override fun buildLoadData(
model: InputStream,
width: Int,
height: Int,
options: Options
): ModelLoader.LoadData<InputStream>? {
return ModelLoader.LoadData(ObjectKey(model), Fetcher(model))
}
override fun handles(model: InputStream): Boolean {
return true
}
class Fetcher(private val stream: InputStream) : DataFetcher<InputStream> {
override fun getDataClass(): Class<InputStream> {
return InputStream::class.java
}
override fun cleanup() {
try {
stream.close()
} catch (e: IOException) {
// Do nothing
}
}
override fun getDataSource(): DataSource {
return DataSource.LOCAL
}
override fun cancel() {
// Do nothing
}
override fun loadData(
priority: Priority,
callback: DataFetcher.DataCallback<in InputStream>
) {
callback.onDataReady(stream)
}
}
/**
* Factory class for creating [PassthroughModelLoader] instances.
*/
class Factory : ModelLoaderFactory<InputStream, InputStream> {
override fun build(
multiFactory: MultiModelLoaderFactory
): ModelLoader<InputStream, InputStream> {
return PassthroughModelLoader()
}
override fun teardown() {}
}
}

View File

@ -1,39 +0,0 @@
package eu.kanade.tachiyomi.data.glide
import android.content.Context
import com.bumptech.glide.Glide
import com.bumptech.glide.GlideBuilder
import com.bumptech.glide.Registry
import com.bumptech.glide.annotation.GlideModule
import com.bumptech.glide.integration.okhttp3.OkHttpUrlLoader
import com.bumptech.glide.load.DecodeFormat
import com.bumptech.glide.load.engine.cache.InternalCacheDiskCacheFactory
import com.bumptech.glide.load.model.GlideUrl
import com.bumptech.glide.module.AppGlideModule
import com.bumptech.glide.request.RequestOptions
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.network.NetworkHelper
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.io.InputStream
/**
* Class used to update Glide module settings
*/
@GlideModule
class TachiGlideModule : AppGlideModule() {
override fun applyOptions(context: Context, builder: GlideBuilder) {
builder.setDiskCache(InternalCacheDiskCacheFactory(context, 50 * 1024 * 1024))
builder.setDefaultRequestOptions(RequestOptions().format(DecodeFormat.PREFER_RGB_565))
}
override fun registerComponents(context: Context, glide: Glide, registry: Registry) {
val networkFactory = OkHttpUrlLoader.Factory(Injekt.get<NetworkHelper>().client)
registry.replace(GlideUrl::class.java, InputStream::class.java, networkFactory)
registry.append(Manga::class.java, InputStream::class.java, MangaModelLoader.Factory())
registry.append(InputStream::class.java, InputStream::class.java, PassthroughModelLoader
.Factory())
}
}

View File

@ -6,6 +6,7 @@ import android.app.Service
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.graphics.BitmapFactory import android.graphics.BitmapFactory
import android.graphics.drawable.BitmapDrawable
import android.os.Build import android.os.Build
import android.os.IBinder import android.os.IBinder
import android.os.PowerManager import android.os.PowerManager
@ -13,16 +14,18 @@ import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationCompat.GROUP_ALERT_SUMMARY import androidx.core.app.NotificationCompat.GROUP_ALERT_SUMMARY
import androidx.core.app.NotificationManagerCompat import androidx.core.app.NotificationManagerCompat
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import coil.Coil
import coil.request.CachePolicy
import coil.request.LoadRequest
import coil.transform.CircleCropTransformation
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.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.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.database.models.MangaImpl
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.glide.GlideApp
import eu.kanade.tachiyomi.data.library.LibraryUpdateRanker.rankingScheme import eu.kanade.tachiyomi.data.library.LibraryUpdateRanker.rankingScheme
import eu.kanade.tachiyomi.data.library.LibraryUpdateService.Companion.start import eu.kanade.tachiyomi.data.library.LibraryUpdateService.Companion.start
import eu.kanade.tachiyomi.data.notification.NotificationReceiver import eu.kanade.tachiyomi.data.notification.NotificationReceiver
@ -56,7 +59,6 @@ import timber.log.Timber
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import java.util.ArrayList import java.util.ArrayList
import java.util.Date
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicInteger import java.util.concurrent.atomic.AtomicInteger
@ -530,10 +532,16 @@ class LibraryUpdateService(
val thumbnailUrl = manga.thumbnail_url val thumbnailUrl = manga.thumbnail_url
manga.copyFrom(networkManga) manga.copyFrom(networkManga)
manga.initialized = true manga.initialized = true
db.insertManga(manga).executeAsBlocking() // load new covers in background
if (thumbnailUrl != networkManga.thumbnail_url && !manga.hasCustomCover()) { if (!manga.hasCustomCover()) {
MangaImpl.setLastCoverFetch(manga.id!!, Date().time) val request = LoadRequest.Builder(this@LibraryUpdateService)
.data(manga)
.memoryCachePolicy(CachePolicy.DISABLED)
.build()
Coil.imageLoader(this@LibraryUpdateService).execute(request)
} }
db.insertManga(manga).executeAsBlocking()
} }
} }
} }
@ -604,10 +612,12 @@ class LibraryUpdateService(
notifications.add(Pair(notification(Notifications.CHANNEL_NEW_CHAPTERS) { notifications.add(Pair(notification(Notifications.CHANNEL_NEW_CHAPTERS) {
setSmallIcon(R.drawable.ic_tachi) setSmallIcon(R.drawable.ic_tachi)
try { try {
val icon = GlideApp.with(this@LibraryUpdateService)
.asBitmap().load(manga).dontTransform().centerCrop().circleCrop() val request = LoadRequest.Builder(this@LibraryUpdateService).data(manga)
.override(256, 256).submit().get() .transformations(CircleCropTransformation()).size(width = 256, height = 256)
setLargeIcon(icon) .target { drawable -> setLargeIcon((drawable as BitmapDrawable).bitmap) }.build()
Coil.imageLoader(this@LibraryUpdateService).execute(request)
} catch (e: Exception) { } catch (e: Exception) {
} }
setGroupAlertBehavior(GROUP_ALERT_SUMMARY) setGroupAlertBehavior(GROUP_ALERT_SUMMARY)

View File

@ -47,8 +47,8 @@ class LocalSource(private val context: Context) : CatalogueSource {
return null return null
} }
val cover = File("${dir.absolutePath}/${manga.url}", COVER_NAME) val cover = File("${dir.absolutePath}/${manga.url}", COVER_NAME)
if (cover.exists()) cover.delete()
// It might not exist if using the external SD card
cover.parentFile?.mkdirs() cover.parentFile?.mkdirs()
input.use { input.use {
cover.outputStream().use { cover.outputStream().use {

View File

@ -4,15 +4,18 @@ import android.content.res.ColorStateList
import android.graphics.Color import android.graphics.Color
import android.view.View import android.view.View
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import coil.api.clear
import coil.api.load
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.glide.GlideApp
import eu.kanade.tachiyomi.extension.model.Extension import eu.kanade.tachiyomi.extension.model.Extension
import eu.kanade.tachiyomi.extension.model.InstallStep import eu.kanade.tachiyomi.extension.model.InstallStep
import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder
import eu.kanade.tachiyomi.util.system.LocaleHelper import eu.kanade.tachiyomi.util.system.LocaleHelper
import eu.kanade.tachiyomi.util.system.getResourceColor import eu.kanade.tachiyomi.util.system.getResourceColor
import eu.kanade.tachiyomi.util.view.resetStrokeColor import eu.kanade.tachiyomi.util.view.resetStrokeColor
import eu.kanade.tachiyomi.widget.CoverViewTarget
import kotlinx.android.synthetic.main.extension_card_item.* import kotlinx.android.synthetic.main.extension_card_item.*
import kotlinx.android.synthetic.main.source_global_search_controller_card_item.*
class ExtensionHolder(view: View, val adapter: ExtensionAdapter) : class ExtensionHolder(view: View, val adapter: ExtensionAdapter) :
BaseFlexibleViewHolder(view, adapter) { BaseFlexibleViewHolder(view, adapter) {
@ -35,11 +38,12 @@ class ExtensionHolder(view: View, val adapter: ExtensionAdapter) :
itemView.context.getString(R.string.untrusted).toUpperCase() itemView.context.getString(R.string.untrusted).toUpperCase()
} }
GlideApp.with(itemView.context).clear(edit_button) edit_button.clear()
if (extension is Extension.Available) { if (extension is Extension.Available) {
GlideApp.with(itemView.context) edit_button.load(extension.iconUrl) {
.load(extension.iconUrl) target(CoverViewTarget(edit_button, progress))
.into(edit_button) }
} else { } else {
extension.getApplicationIcon(itemView.context)?.let { edit_button.setImageDrawable(it) } extension.getApplicationIcon(itemView.context)?.let { edit_button.setImageDrawable(it) }
} }

View File

@ -4,11 +4,11 @@ import android.app.Activity
import android.view.Gravity import android.view.Gravity
import android.view.View import android.view.View
import android.widget.FrameLayout import android.widget.FrameLayout
import com.bumptech.glide.load.engine.DiskCacheStrategy import coil.api.clear
import com.bumptech.glide.signature.ObjectKey import coil.api.loadAny
import coil.size.Precision
import coil.size.Scale
import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.database.models.MangaImpl
import eu.kanade.tachiyomi.data.glide.GlideApp
import eu.kanade.tachiyomi.util.view.gone import eu.kanade.tachiyomi.util.view.gone
import eu.kanade.tachiyomi.util.view.visibleIf import eu.kanade.tachiyomi.util.view.visibleIf
import kotlinx.android.synthetic.main.manga_grid_item.* import kotlinx.android.synthetic.main.manga_grid_item.*
@ -65,29 +65,26 @@ class LibraryGridHolder(
setReadingButton(item) setReadingButton(item)
// Update the cover. // Update the cover.
if (item.manga.thumbnail_url == null) GlideApp.with(view.context).clear(cover_thumbnail) if (item.manga.thumbnail_url == null) cover_thumbnail.clear()
else { else {
val id = item.manga.id ?: return
if (cover_thumbnail.height == 0) { if (cover_thumbnail.height == 0) {
val oldPos = adapterPosition val oldPos = adapterPosition
adapter.recyclerView.post { adapter.recyclerView.post {
if (oldPos == adapterPosition) if (oldPos == adapterPosition)
setCover(item.manga, id) setCover(item.manga)
} }
} else setCover(item.manga, id) } else setCover(item.manga)
} }
} }
private fun setCover(manga: Manga, id: Long) { private fun setCover(manga: Manga) {
if ((adapter.recyclerView.context as? Activity)?.isDestroyed == true) return if ((adapter.recyclerView.context as? Activity)?.isDestroyed == true) return
GlideApp.with(adapter.recyclerView.context).load(manga) cover_thumbnail.loadAny(manga) {
.diskCacheStrategy(DiskCacheStrategy.RESOURCE) if (!fixedSize) {
.signature(ObjectKey(MangaImpl.getLastCoverFetch(id).toString())) precision(Precision.INEXACT)
.apply { scale(Scale.FIT)
if (fixedSize) centerCrop() }
else override(cover_thumbnail.maxHeight)
} }
.into(cover_thumbnail)
} }
private fun playButtonClicked() { private fun playButtonClicked() {

View File

@ -2,19 +2,19 @@ package eu.kanade.tachiyomi.ui.library
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import com.bumptech.glide.Glide import coil.api.clear
import com.bumptech.glide.load.engine.DiskCacheStrategy import coil.api.loadAny
import com.bumptech.glide.signature.ObjectKey import coil.transform.RoundedCornersTransformation
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.MangaImpl
import eu.kanade.tachiyomi.data.glide.GlideApp
import eu.kanade.tachiyomi.util.system.dpToPx import eu.kanade.tachiyomi.util.system.dpToPx
import eu.kanade.tachiyomi.util.view.gone import eu.kanade.tachiyomi.util.view.gone
import eu.kanade.tachiyomi.util.view.updateLayoutParams import eu.kanade.tachiyomi.util.view.updateLayoutParams
import eu.kanade.tachiyomi.util.view.visible import eu.kanade.tachiyomi.util.view.visible
import eu.kanade.tachiyomi.util.view.visibleIf import eu.kanade.tachiyomi.util.view.visibleIf
import kotlinx.android.synthetic.main.manga_list_item.* import kotlinx.android.synthetic.main.manga_list_item.*
import kotlinx.android.synthetic.main.manga_list_item.title
import kotlinx.android.synthetic.main.manga_list_item.view.* import kotlinx.android.synthetic.main.manga_list_item.view.*
import kotlinx.android.synthetic.main.recently_read_item.*
import kotlinx.android.synthetic.main.unread_download_badge.* import kotlinx.android.synthetic.main.unread_download_badge.*
/** /**
@ -78,15 +78,13 @@ class LibraryListHolder(
} }
// Update the cover. // Update the cover.
if (item.manga.thumbnail_url == null) Glide.with(view.context).clear(cover_thumbnail) if (item.manga.thumbnail_url == null) {
else { cover_thumbnail.clear()
} else {
val id = item.manga.id ?: return val id = item.manga.id ?: return
cover_thumbnail.loadAny(item.manga) {
GlideApp.with(view.context).load(item.manga) transformations(RoundedCornersTransformation(2f, 2f, 2f, 2f))
.diskCacheStrategy(DiskCacheStrategy.RESOURCE) }
.signature(ObjectKey(MangaImpl.getLastCoverFetch(id).toString()))
.centerCrop()
.into(cover_thumbnail)
} }
} }

View File

@ -641,7 +641,7 @@ class LibraryPresenter(
val mangaToDelete = mangas.distinctBy { it.id } val mangaToDelete = mangas.distinctBy { it.id }
mangaToDelete.forEach { manga -> mangaToDelete.forEach { manga ->
db.resetMangaInfo(manga).executeOnIO() db.resetMangaInfo(manga).executeOnIO()
coverCache.deleteFromCache(manga.thumbnail_url) coverCache.deleteFromCache(manga)
val source = sourceManager.get(manga.source) as? HttpSource val source = sourceManager.get(manga.source) as? HttpSource
if (source != null) if (source != null)
downloadManager.deleteManga(manga, source) downloadManager.deleteManga(manga, source)

View File

@ -4,15 +4,12 @@ import android.app.Dialog
import android.net.Uri import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.view.View import android.view.View
import coil.api.loadAny
import com.afollestad.materialdialogs.MaterialDialog import com.afollestad.materialdialogs.MaterialDialog
import com.afollestad.materialdialogs.customview.customView import com.afollestad.materialdialogs.customview.customView
import com.bumptech.glide.load.engine.DiskCacheStrategy
import com.bumptech.glide.signature.ObjectKey
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.database.models.MangaImpl
import eu.kanade.tachiyomi.data.glide.GlideApp
import eu.kanade.tachiyomi.source.LocalSource import eu.kanade.tachiyomi.source.LocalSource
import eu.kanade.tachiyomi.ui.base.controller.DialogController import eu.kanade.tachiyomi.ui.base.controller.DialogController
import kotlinx.android.synthetic.main.edit_manga_dialog.view.* import kotlinx.android.synthetic.main.edit_manga_dialog.view.*
@ -60,13 +57,7 @@ class EditMangaDialog : DialogController {
} }
fun onViewCreated(view: View) { fun onViewCreated(view: View) {
GlideApp.with(view.context) view.manga_cover.loadAny(manga)
.asDrawable()
.load(manga)
.diskCacheStrategy(DiskCacheStrategy.RESOURCE)
.signature(ObjectKey(MangaImpl.getLastCoverFetch(manga.id!!).toString()))
.dontAnimate()
.into(view.manga_cover)
val isLocal = manga.source == LocalSource.ID val isLocal = manga.source == LocalSource.ID
if (isLocal) { if (isLocal) {
@ -93,7 +84,7 @@ class EditMangaDialog : DialogController {
} }
fun updateCover(uri: Uri) { fun updateCover(uri: Uri) {
GlideApp.with(dialogView!!.context).load(uri).into(dialogView!!.manga_cover) dialogView!!.manga_cover.loadAny(uri)
customCoverUri = uri customCoverUri = uri
} }

View File

@ -11,11 +11,9 @@ import android.content.ClipData
import android.content.ClipboardManager import android.content.ClipboardManager
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.graphics.Bitmap
import android.graphics.Color import android.graphics.Color
import android.graphics.Rect import android.graphics.Rect
import android.graphics.drawable.BitmapDrawable import android.graphics.drawable.BitmapDrawable
import android.graphics.drawable.Drawable
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
@ -24,7 +22,6 @@ import android.view.MenuInflater
import android.view.MenuItem import android.view.MenuItem
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.view.ViewPropertyAnimator
import android.view.WindowInsets import android.view.WindowInsets
import android.view.animation.DecelerateInterpolator import android.view.animation.DecelerateInterpolator
import android.view.inputmethod.InputMethodManager import android.view.inputmethod.InputMethodManager
@ -41,16 +38,16 @@ import androidx.transition.ChangeBounds
import androidx.transition.ChangeImageTransform import androidx.transition.ChangeImageTransform
import androidx.transition.TransitionManager import androidx.transition.TransitionManager
import androidx.transition.TransitionSet import androidx.transition.TransitionSet
import coil.Coil
import coil.api.clear
import coil.api.loadAny
import coil.request.LoadRequest
import com.afollestad.materialdialogs.MaterialDialog import com.afollestad.materialdialogs.MaterialDialog
import com.afollestad.materialdialogs.checkbox.checkBoxPrompt import com.afollestad.materialdialogs.checkbox.checkBoxPrompt
import com.afollestad.materialdialogs.checkbox.isCheckPromptChecked import com.afollestad.materialdialogs.checkbox.isCheckPromptChecked
import com.afollestad.materialdialogs.list.listItems import com.afollestad.materialdialogs.list.listItems
import com.bluelinelabs.conductor.ControllerChangeHandler import com.bluelinelabs.conductor.ControllerChangeHandler
import com.bluelinelabs.conductor.ControllerChangeType import com.bluelinelabs.conductor.ControllerChangeType
import com.bumptech.glide.load.engine.DiskCacheStrategy
import com.bumptech.glide.request.target.CustomTarget
import com.bumptech.glide.request.transition.Transition
import com.bumptech.glide.signature.ObjectKey
import com.google.android.material.snackbar.BaseTransientBottomBar import com.google.android.material.snackbar.BaseTransientBottomBar
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import eu.davidea.flexibleadapter.FlexibleAdapter import eu.davidea.flexibleadapter.FlexibleAdapter
@ -60,10 +57,8 @@ 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.Manga import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.database.models.MangaImpl
import eu.kanade.tachiyomi.data.download.DownloadService import eu.kanade.tachiyomi.data.download.DownloadService
import eu.kanade.tachiyomi.data.download.model.Download import eu.kanade.tachiyomi.data.download.model.Download
import eu.kanade.tachiyomi.data.glide.GlideApp
import eu.kanade.tachiyomi.data.notification.NotificationReceiver import eu.kanade.tachiyomi.data.notification.NotificationReceiver
import eu.kanade.tachiyomi.data.preference.getOrDefault import eu.kanade.tachiyomi.data.preference.getOrDefault
import eu.kanade.tachiyomi.data.track.model.TrackSearch import eu.kanade.tachiyomi.data.track.model.TrackSearch
@ -165,13 +160,10 @@ class MangaDetailsController : BaseController,
var toolbarIsColored = false var toolbarIsColored = false
private var snack: Snackbar? = null private var snack: Snackbar? = null
val fromCatalogue = args.getBoolean(FROM_CATALOGUE_EXTRA, false) val fromCatalogue = args.getBoolean(FROM_CATALOGUE_EXTRA, false)
var coverDrawable: Drawable? = null
private var trackingBottomSheet: TrackingBottomSheet? = null private var trackingBottomSheet: TrackingBottomSheet? = null
private var startingDLChapterPos: Int? = null private var startingDLChapterPos: Int? = null
private var editMangaDialog: EditMangaDialog? = null private var editMangaDialog: EditMangaDialog? = null
var refreshTracker: Int? = null var refreshTracker: Int? = null
private var textAnim: ViewPropertyAnimator? = null
private var scrollAnim: ViewPropertyAnimator? = null
var chapterPopupMenu: Pair<Int, PopupMenu>? = null var chapterPopupMenu: Pair<Int, PopupMenu>? = null
private var query = "" private var query = ""
@ -313,29 +305,23 @@ class MangaDetailsController : BaseController,
} }
} }
/** Get the color of the manga cover based on the current theme */ /** Get the color of the manga cover*/
fun setPaletteColor() { fun setPaletteColor() {
val view = view ?: return val view = view ?: return
coverColor = null coverColor = null
GlideApp.with(view.context).load(manga).diskCacheStrategy(DiskCacheStrategy.RESOURCE)
.signature(ObjectKey(MangaImpl.getLastCoverFetch(manga!!.id!!).toString())) val request = LoadRequest.Builder(view.context).data(manga).allowHardware(false)
.into(object : CustomTarget<Drawable>() { .target { drawable ->
override fun onResourceReady( val bitmap = (drawable as BitmapDrawable).bitmap
resource: Drawable, // Generate the Palette on a background thread.
transition: Transition<in Drawable>? Palette.from(bitmap).generate {
) {
coverDrawable = resource
val bitmapCover = resource as? BitmapDrawable ?: return
Palette.from(bitmapCover.bitmap).generate {
if (recycler == null || it == null) return@generate if (recycler == null || it == null) return@generate
val colorBack = view.context.getResourceColor( val colorBack = view.context.getResourceColor(
android.R.attr.colorBackground android.R.attr.colorBackground
) )
val backDropColor = if (!view.context.isInNightMode()) { // this makes the color more consistent regardless of theme
it.getLightVibrantColor(colorBack) val backDropColor = ColorUtils.blendARGB(it.getVibrantColor(colorBack), colorBack, .35f)
} else {
it.getDarkVibrantColor(colorBack)
}
coverColor = backDropColor coverColor = backDropColor
getHeader()?.setBackDrop(backDropColor) getHeader()?.setBackDrop(backDropColor)
if (toolbarIsColored) { if (toolbarIsColored) {
@ -344,11 +330,14 @@ class MangaDetailsController : BaseController,
activity?.window?.statusBarColor = translucentColor activity?.window?.statusBarColor = translucentColor
} }
} }
getHeader()?.updateCover(presenter.manga) }.build()
Coil.imageLoader(view.context).execute(request)
} }
override fun onLoadCleared(placeholder: Drawable?) {} fun resetCovers() {
}) manga_cover_full.clear()
manga_cover_full.loadAny(manga)
getHeader()?.updateCover(manga!!, true)
} }
/** Set toolbar theme for themes that are inverted (ie. light blue theme) */ /** Set toolbar theme for themes that are inverted (ie. light blue theme) */
@ -404,12 +393,15 @@ class MangaDetailsController : BaseController,
super.onActivityResumed(activity) super.onActivityResumed(activity)
presenter.isLockedFromSearch = SecureActivityDelegate.shouldBeLocked() presenter.isLockedFromSearch = SecureActivityDelegate.shouldBeLocked()
presenter.headerItem.isLocked = presenter.isLockedFromSearch presenter.headerItem.isLocked = presenter.isLockedFromSearch
manga!!.thumbnail_url = presenter.refreshMangaFromDb().thumbnail_url
presenter.fetchChapters(refreshTracker == null) presenter.fetchChapters(refreshTracker == null)
if (refreshTracker != null) { if (refreshTracker != null) {
trackingBottomSheet?.refreshItem(refreshTracker ?: 0) trackingBottomSheet?.refreshItem(refreshTracker ?: 0)
presenter.refreshTracking() presenter.refreshTracking()
refreshTracker = null refreshTracker = null
} }
// reset the covers and palette cause user might have set a custom cover
presenter.forceUpdateCovers(false)
val isCurrentController = router?.backstack?.lastOrNull()?.controller() == val isCurrentController = router?.backstack?.lastOrNull()?.controller() ==
this this
if (isCurrentController) { if (isCurrentController) {
@ -774,7 +766,7 @@ class MangaDetailsController : BaseController,
), waitForPositiveButton = false, selection = { _, index, _ -> ), waitForPositiveButton = false, selection = { _, index, _ ->
when (index) { when (index) {
0 -> changeCover() 0 -> changeCover()
else -> presenter.clearCover() else -> presenter.clearCustomCover()
} }
}).show() }).show()
} else { } else {
@ -810,20 +802,16 @@ class MangaDetailsController : BaseController,
//endregion //endregion
override fun prepareToShareManga() { override fun prepareToShareManga() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && coverDrawable != null) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
GlideApp.with(activity!!).asBitmap().load(presenter.manga).into(object : val request = LoadRequest.Builder(activity!!).data(manga).target(onError = {
CustomTarget<Bitmap>() { shareManga()
override fun onResourceReady(resource: Bitmap, transition: Transition<in Bitmap>?) { }, onSuccess = {
presenter.shareManga(resource) presenter.shareManga((it as BitmapDrawable).bitmap)
} }).build()
Coil.imageLoader(activity!!).execute(request)
override fun onLoadCleared(placeholder: Drawable?) {} } else {
override fun onLoadFailed(errorDrawable: Drawable?) {
shareManga() shareManga()
} }
})
else shareManga()
} }
fun shareManga(cover: File? = null) { fun shareManga(cover: File? = null) {
@ -1257,7 +1245,6 @@ class MangaDetailsController : BaseController,
if (editMangaDialog != null) editMangaDialog?.updateCover(uri) if (editMangaDialog != null) editMangaDialog?.updateCover(uri)
else { else {
presenter.editCoverWithStream(uri) presenter.editCoverWithStream(uri)
setPaletteColor()
} }
} catch (error: IOException) { } catch (error: IOException) {
activity.toast(R.string.failed_to_update_cover) activity.toast(R.string.failed_to_update_cover)
@ -1271,10 +1258,9 @@ class MangaDetailsController : BaseController,
currentAnimator?.cancel() currentAnimator?.cancel()
// Load the high-resolution "zoomed-in" image. // Load the high-resolution "zoomed-in" image.
manga_cover_full?.loadAny(manga)
val expandedImageView = manga_cover_full ?: return val expandedImageView = manga_cover_full ?: return
val fullBackdrop = full_backdrop val fullBackdrop = full_backdrop
val image = coverDrawable ?: return
expandedImageView.setImageDrawable(image)
// Hide the thumbnail and show the zoomed-in view. When the animation // Hide the thumbnail and show the zoomed-in view. When the animation
// begins, it will position the zoomed-in view in the place of the // begins, it will position the zoomed-in view in the place of the

View File

@ -11,7 +11,6 @@ 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.database.models.MangaCategory import eu.kanade.tachiyomi.data.database.models.MangaCategory
import eu.kanade.tachiyomi.data.database.models.MangaImpl
import eu.kanade.tachiyomi.data.database.models.Track import eu.kanade.tachiyomi.data.database.models.Track
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
@ -27,6 +26,7 @@ import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.fetchChapterListAsync import eu.kanade.tachiyomi.source.fetchChapterListAsync
import eu.kanade.tachiyomi.source.fetchMangaDetailsAsync import eu.kanade.tachiyomi.source.fetchMangaDetailsAsync
import eu.kanade.tachiyomi.source.model.SChapter import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.ui.manga.chapter.ChapterItem import eu.kanade.tachiyomi.ui.manga.chapter.ChapterItem
import eu.kanade.tachiyomi.ui.manga.track.TrackItem import eu.kanade.tachiyomi.ui.manga.track.TrackItem
import eu.kanade.tachiyomi.ui.security.SecureActivityDelegate import eu.kanade.tachiyomi.ui.security.SecureActivityDelegate
@ -368,6 +368,12 @@ class MangaDetailsPresenter(
if (update) controller.updateChapters(this.chapters) if (update) controller.updateChapters(this.chapters)
} }
fun refreshMangaFromDb(): Manga {
val dbManga = db.getManga(manga.id!!).executeAsBlocking()
manga.copyFrom(dbManga!!)
return dbManga
}
/** Refresh Manga Info and Chapter List (not tracking) */ /** Refresh Manga Info and Chapter List (not tracking) */
fun refreshAll() { fun refreshAll() {
if (controller.isNotOnline() && manga.source != LocalSource.ID) return if (controller.isNotOnline() && manga.source != LocalSource.ID) return
@ -398,13 +404,16 @@ class MangaDetailsPresenter(
if (networkManga != null) { if (networkManga != null) {
manga.copyFrom(networkManga) manga.copyFrom(networkManga)
manga.initialized = true manga.initialized = true
db.insertManga(manga).executeAsBlocking()
if (thumbnailUrl != networkManga.thumbnail_url && !manga.hasCustomCover()) { if (shouldUpdateCover(thumbnailUrl, networkManga)) {
coverCache.deleteFromCache(thumbnailUrl) coverCache.deleteFromCache(manga, false)
MangaImpl.setLastCoverFetch(manga.id!!, Date().time) manga.thumbnail_url = networkManga.thumbnail_url
withContext(Dispatchers.Main) { controller.setPaletteColor() } withContext(Dispatchers.Main) {
forceUpdateCovers()
} }
} }
db.insertManga(manga).executeAsBlocking()
}
val finChapters = chapters.await() val finChapters = chapters.await()
if (finChapters.isNotEmpty()) { if (finChapters.isNotEmpty()) {
val newChapters = syncChaptersWithSource(db, finChapters, manga, source) val newChapters = syncChaptersWithSource(db, finChapters, manga, source)
@ -451,6 +460,19 @@ class MangaDetailsPresenter(
} }
} }
private fun shouldUpdateCover(thumbnailUrl: String?, networkManga: SManga): Boolean {
val refreshCovers = preferences.refreshCoversToo().getOrDefault()
if (thumbnailUrl == networkManga.thumbnail_url && !refreshCovers) {
return false
}
if (thumbnailUrl != networkManga.thumbnail_url && !manga.hasCustomCover()) {
return true
}
if (manga.hasCustomCover()) return false
return refreshCovers
}
/** /**
* Requests an updated list of chapters from the source. * Requests an updated list of chapters from the source.
*/ */
@ -588,7 +610,9 @@ class MangaDetailsPresenter(
manga.favorite = !manga.favorite manga.favorite = !manga.favorite
when (manga.favorite) { when (manga.favorite) {
true -> manga.date_added = Date().time true -> {
manga.date_added = Date().time
}
false -> manga.date_added = 0 false -> manga.date_added = 0
} }
@ -639,7 +663,7 @@ class MangaDetailsPresenter(
} }
fun confirmDeletion() { fun confirmDeletion() {
coverCache.deleteFromCache(manga.thumbnail_url) coverCache.deleteFromCache(manga)
db.resetMangaInfo(manga).executeAsBlocking() db.resetMangaInfo(manga).executeAsBlocking()
downloadManager.deleteManga(manga, source) downloadManager.deleteManga(manga, source)
asyncUpdateMangaAndChapters(true) asyncUpdateMangaAndChapters(true)
@ -707,36 +731,41 @@ class MangaDetailsPresenter(
db.updateMangaInfo(manga).executeAsBlocking() db.updateMangaInfo(manga).executeAsBlocking()
} }
if (uri != null) editCoverWithStream(uri) if (uri != null) editCoverWithStream(uri)
controller.updateHeader()
} }
fun clearCover() { /**
* Remvoe custom cover
*/
fun clearCustomCover() {
if (manga.hasCustomCover()) { if (manga.hasCustomCover()) {
coverCache.deleteFromCache(manga.thumbnail_url!!) coverCache.deleteFromCache(manga)
manga.thumbnail_url = manga.thumbnail_url?.removePrefix("Custom-") manga.removeCustomThumbnailUrl()
db.insertManga(manga).executeAsBlocking() db.insertManga(manga).executeAsBlocking()
MangaImpl.setLastCoverFetch(manga.id!!, Date().time) forceUpdateCovers()
controller.updateHeader()
controller.setPaletteColor()
} }
} }
fun forceUpdateCovers(deleteCache: Boolean = true) {
if (deleteCache) coverCache.deleteFromCache(manga)
controller.setPaletteColor()
controller.resetCovers()
}
fun editCoverWithStream(uri: Uri): Boolean { fun editCoverWithStream(uri: Uri): Boolean {
val inputStream = val inputStream =
downloadManager.context.contentResolver.openInputStream(uri) ?: return false downloadManager.context.contentResolver.openInputStream(uri) ?: return false
if (manga.source == LocalSource.ID) { if (manga.source == LocalSource.ID) {
LocalSource.updateCover(downloadManager.context, manga, inputStream) LocalSource.updateCover(downloadManager.context, manga, inputStream)
MangaImpl.setLastCoverFetch(manga.id!!, Date().time) forceUpdateCovers()
return true return true
} }
if (manga.favorite) { if (manga.favorite) {
if (!manga.hasCustomCover()) { coverCache.deleteFromCache(manga)
manga.thumbnail_url = "Custom-${manga.thumbnail_url ?: manga.id!!}" manga.setCustomThumbnailUrl()
db.insertManga(manga).executeAsBlocking() db.insertManga(manga).executeAsBlocking()
} coverCache.copyToCache(manga, inputStream)
coverCache.copyToCache(manga.thumbnail_url!!, inputStream) forceUpdateCovers(false)
MangaImpl.setLastCoverFetch(manga.id!!, Date().time)
return true return true
} }
return false return false

View File

@ -9,14 +9,11 @@ import android.view.ViewGroup
import androidx.constraintlayout.widget.ConstraintLayout import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.graphics.ColorUtils import androidx.core.graphics.ColorUtils
import com.bumptech.glide.load.engine.DiskCacheStrategy import coil.api.clear
import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions import coil.api.loadAny
import com.bumptech.glide.signature.ObjectKey
import com.google.android.material.button.MaterialButton import com.google.android.material.button.MaterialButton
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.database.models.MangaImpl
import eu.kanade.tachiyomi.data.glide.GlideApp
import eu.kanade.tachiyomi.source.LocalSource import eu.kanade.tachiyomi.source.LocalSource
import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder
@ -287,11 +284,15 @@ class MangaHeaderHolder(
val presenter = adapter.delegate.mangaPresenter() val presenter = adapter.delegate.mangaPresenter()
val tracked = presenter.isTracked() val tracked = presenter.isTracked()
with(track_button) { with(track_button) {
text = itemView.context.getString(if (tracked) R.string.tracked text = itemView.context.getString(
else R.string.tracking) if (tracked) R.string.tracked
else R.string.tracking
)
icon = ContextCompat.getDrawable(itemView.context, if (tracked) R.drawable icon = ContextCompat.getDrawable(
.ic_check_white_24dp else R.drawable.ic_sync_black_24dp) itemView.context, if (tracked) R.drawable
.ic_check_white_24dp else R.drawable.ic_sync_black_24dp
)
checked(tracked) checked(tracked)
} }
} }
@ -307,22 +308,18 @@ class MangaHeaderHolder(
} }
} }
fun updateCover(manga: Manga) { fun updateCover(manga: Manga, forceUpdate: Boolean = false) {
if (!isCached(manga)) return if (!isCached(manga) && !forceUpdate) return
GlideApp.with(view.context).load(manga).diskCacheStrategy(DiskCacheStrategy.RESOURCE) manga_cover.clear()
.signature(ObjectKey(MangaImpl.getLastCoverFetch(manga.id!!).toString())) backdrop.clear()
.into(manga_cover) manga_cover.loadAny(manga)
GlideApp.with(view.context).load(manga).diskCacheStrategy(DiskCacheStrategy.RESOURCE) backdrop.loadAny(manga)
.signature(ObjectKey(MangaImpl.getLastCoverFetch(manga.id!!).toString())).centerCrop()
.transition(DrawableTransitionOptions.withCrossFade()).into(backdrop)
} }
private fun isCached(manga: Manga): Boolean { private fun isCached(manga: Manga): Boolean {
if (manga.source == LocalSource.ID) return true if (manga.source == LocalSource.ID) return true
val coverCache = adapter.delegate.mangaPresenter().coverCache
manga.thumbnail_url?.let { manga.thumbnail_url?.let {
return if (manga.favorite) coverCache.getCoverFile(it).exists() return adapter.delegate.mangaPresenter().coverCache.getCoverFile(manga).exists()
else true
} }
return manga.initialized return manga.initialized
} }

View File

@ -4,10 +4,9 @@ import android.content.Context
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.ArrayAdapter import android.widget.ArrayAdapter
import com.bumptech.glide.load.engine.DiskCacheStrategy import coil.api.clear
import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions import coil.api.load
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.glide.GlideApp
import eu.kanade.tachiyomi.data.track.model.TrackSearch import eu.kanade.tachiyomi.data.track.model.TrackSearch
import eu.kanade.tachiyomi.util.view.gone import eu.kanade.tachiyomi.util.view.gone
import eu.kanade.tachiyomi.util.view.inflate import eu.kanade.tachiyomi.util.view.inflate
@ -46,12 +45,9 @@ class TrackSearchAdapter(context: Context) :
fun onSetValues(track: TrackSearch) { fun onSetValues(track: TrackSearch) {
view.track_search_title.text = track.title view.track_search_title.text = track.title
view.track_search_summary.text = track.summary view.track_search_summary.text = track.summary
GlideApp.with(view.context).clear(view.track_search_cover) view.track_search_cover.clear()
if (!track.cover_url.isNullOrEmpty()) { if (!track.cover_url.isNullOrEmpty()) {
GlideApp.with(view.context).load(track.cover_url) view.track_search_cover.load(track.cover_url)
.diskCacheStrategy(DiskCacheStrategy.RESOURCE).centerCrop()
.transition(DrawableTransitionOptions.withCrossFade())
.into(view.track_search_cover)
} }
if (track.publishing_status.isNullOrBlank()) { if (track.publishing_status.isNullOrBlank()) {

View File

@ -2,10 +2,10 @@ 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 com.bumptech.glide.load.engine.DiskCacheStrategy import coil.api.clear
import coil.api.loadAny
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.glide.GlideApp
import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder
import kotlinx.android.synthetic.main.manga_list_item.* import kotlinx.android.synthetic.main.manga_list_item.*
@ -20,9 +20,7 @@ class MangaHolder(
subtitle.text = item.manga.author?.trim() subtitle.text = item.manga.author?.trim()
// Update the cover. // Update the cover.
GlideApp.with(itemView.context).clear(cover_thumbnail) cover_thumbnail.clear()
GlideApp.with(itemView.context).load(item.manga) cover_thumbnail.loadAny(item.manga)
.diskCacheStrategy(DiskCacheStrategy.RESOURCE).centerCrop().dontAnimate()
.into(cover_thumbnail)
} }
} }

View File

@ -3,12 +3,11 @@ package eu.kanade.tachiyomi.ui.migration.manga.process
import android.view.View import android.view.View
import androidx.appcompat.widget.PopupMenu import androidx.appcompat.widget.PopupMenu
import androidx.constraintlayout.widget.ConstraintLayout import androidx.constraintlayout.widget.ConstraintLayout
import com.bumptech.glide.load.engine.DiskCacheStrategy import coil.Coil
import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions import coil.request.LoadRequest
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.glide.GlideApp
import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.SourceManager import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder
@ -20,7 +19,7 @@ import eu.kanade.tachiyomi.util.view.invisible
import eu.kanade.tachiyomi.util.view.setVectorCompat import eu.kanade.tachiyomi.util.view.setVectorCompat
import eu.kanade.tachiyomi.util.view.visible import eu.kanade.tachiyomi.util.view.visible
import eu.kanade.tachiyomi.util.view.withFadeTransaction import eu.kanade.tachiyomi.util.view.withFadeTransaction
import eu.kanade.tachiyomi.widget.StateImageViewTarget import eu.kanade.tachiyomi.widget.CoverViewTarget
import kotlinx.android.synthetic.main.manga_grid_item.view.* import kotlinx.android.synthetic.main.manga_grid_item.view.*
import kotlinx.android.synthetic.main.migration_process_item.* import kotlinx.android.synthetic.main.migration_process_item.*
import kotlinx.android.synthetic.main.unread_download_badge.view.* import kotlinx.android.synthetic.main.unread_download_badge.view.*
@ -133,14 +132,10 @@ class MigrationProcessHolder(
private fun View.attachManga(manga: Manga, source: Source, isTo: Boolean) { private fun View.attachManga(manga: Manga, source: Source, isTo: Boolean) {
(layoutParams as ConstraintLayout.LayoutParams).verticalBias = 1f (layoutParams as ConstraintLayout.LayoutParams).verticalBias = 1f
progress.gone() progress.gone()
GlideApp.with(view.context.applicationContext).load(manga).apply {
diskCacheStrategy(DiskCacheStrategy.RESOURCE) val request = LoadRequest.Builder(view.context).data(manga)
if (isTo) { .target(CoverViewTarget(cover_thumbnail, progress)).build()
transition(DrawableTransitionOptions.withCrossFade()) Coil.imageLoader(view.context).execute(request)
.into(StateImageViewTarget(cover_thumbnail, progress))
} else
into(cover_thumbnail)
}
compact_title.visible() compact_title.visible()
gradient.visible() gradient.visible()
@ -164,11 +159,15 @@ class MigrationProcessHolder(
val latestChapter = mangaChapters.maxBy { it.chapter_number }?.chapter_number ?: -1f val latestChapter = mangaChapters.maxBy { it.chapter_number }?.chapter_number ?: -1f
if (latestChapter > 0f) { if (latestChapter > 0f) {
subtitle.text = context.getString(R.string.latest_, subtitle.text = context.getString(
DecimalFormat("#.#").format(latestChapter)) R.string.latest_,
DecimalFormat("#.#").format(latestChapter)
)
} else { } else {
subtitle.text = context.getString(R.string.latest_, subtitle.text = context.getString(
context.getString(R.string.unknown)) R.string.latest_,
context.getString(R.string.unknown)
)
} }
} }

View File

@ -10,7 +10,6 @@ import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.data.database.models.Chapter import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.data.database.models.History import eu.kanade.tachiyomi.data.database.models.History
import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.database.models.MangaImpl
import eu.kanade.tachiyomi.data.download.DownloadManager import eu.kanade.tachiyomi.data.download.DownloadManager
import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.preference.getOrDefault import eu.kanade.tachiyomi.data.preference.getOrDefault
@ -536,18 +535,17 @@ class ReaderPresenter(
.fromCallable { .fromCallable {
if (manga.source == LocalSource.ID) { if (manga.source == LocalSource.ID) {
val context = Injekt.get<Application>() val context = Injekt.get<Application>()
coverCache.deleteFromCache(manga)
LocalSource.updateCover(context, manga, stream()) LocalSource.updateCover(context, manga, stream())
R.string.cover_updated R.string.cover_updated
SetAsCoverResult.Success SetAsCoverResult.Success
} else { } else {
val thumbUrl = manga.thumbnail_url ?: throw Exception("Image url not found") manga.thumbnail_url ?: throw Exception("Image url not found")
if (manga.favorite) { if (manga.favorite) {
if (!manga.hasCustomCover()) { coverCache.deleteFromCache(manga)
manga.thumbnail_url = "Custom-${manga.thumbnail_url ?: manga.id!!}" manga.setCustomThumbnailUrl()
db.insertManga(manga).executeAsBlocking() db.insertManga(manga).executeAsBlocking()
} coverCache.copyToCache(manga, stream())
coverCache.copyToCache(manga.thumbnail_url!!, stream())
MangaImpl.setLastCoverFetch(manga.id!!, Date().time)
SetAsCoverResult.Success SetAsCoverResult.Success
} else { } else {
SetAsCoverResult.AddToLibraryFirst SetAsCoverResult.AddToLibraryFirst

View File

@ -2,11 +2,13 @@ package eu.kanade.tachiyomi.ui.reader
import android.content.Context import android.content.Context
import android.graphics.Bitmap import android.graphics.Bitmap
import android.graphics.drawable.BitmapDrawable
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import com.bumptech.glide.load.engine.DiskCacheStrategy import coil.Coil
import coil.request.CachePolicy
import coil.request.LoadRequest
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.glide.GlideApp
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
@ -36,19 +38,19 @@ class SaveImageNotifier(private val context: Context) {
* @param file image file containing downloaded page image. * @param file image file containing downloaded page image.
*/ */
fun onComplete(file: File) { fun onComplete(file: File) {
val bitmap = GlideApp.with(context)
.asBitmap()
.load(file)
.diskCacheStrategy(DiskCacheStrategy.NONE)
.skipMemoryCache(true)
.submit(720, 1280)
.get()
val request = LoadRequest.Builder(context).memoryCachePolicy(CachePolicy.DISABLED).diskCachePolicy(CachePolicy.DISABLED)
.data(file)
.size(720, 1280)
.target(onSuccess = {
val bitmap = (it as BitmapDrawable).bitmap
if (bitmap != null) { if (bitmap != null) {
showCompleteNotification(file, bitmap) showCompleteNotification(file, bitmap)
} else { } else {
onError(null) onError(null)
} }
}).build()
Coil.imageLoader(context).execute(request)
} }
private fun showCompleteNotification(file: File, image: Bitmap) { private fun showCompleteNotification(file: File, image: Bitmap) {

View File

@ -18,19 +18,12 @@ import android.widget.FrameLayout
import android.widget.ImageView import android.widget.ImageView
import android.widget.LinearLayout import android.widget.LinearLayout
import android.widget.TextView import android.widget.TextView
import com.bumptech.glide.load.DataSource import coil.api.loadAny
import com.bumptech.glide.load.engine.DiskCacheStrategy import coil.request.CachePolicy
import com.bumptech.glide.load.engine.GlideException
import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions
import com.bumptech.glide.load.resource.gif.GifDrawable
import com.bumptech.glide.request.RequestListener
import com.bumptech.glide.request.target.Target
import com.bumptech.glide.request.transition.NoTransition
import com.davemorrissey.labs.subscaleview.ImageSource import com.davemorrissey.labs.subscaleview.ImageSource
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
import com.github.chrisbanes.photoview.PhotoView import com.github.chrisbanes.photoview.PhotoView
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.glide.GlideApp
import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
@ -43,6 +36,7 @@ import eu.kanade.tachiyomi.util.system.isInNightMode
import eu.kanade.tachiyomi.util.system.launchUI import eu.kanade.tachiyomi.util.system.launchUI
import eu.kanade.tachiyomi.util.view.gone import eu.kanade.tachiyomi.util.view.gone
import eu.kanade.tachiyomi.util.view.visible import eu.kanade.tachiyomi.util.view.visible
import eu.kanade.tachiyomi.widget.GifViewTarget
import eu.kanade.tachiyomi.widget.ViewPagerAdapter import eu.kanade.tachiyomi.widget.ViewPagerAdapter
import kotlinx.coroutines.Dispatchers.Default import kotlinx.coroutines.Dispatchers.Default
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@ -487,37 +481,11 @@ 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) {
GlideApp.with(this) this.loadAny(stream.readBytes()) {
.load(stream) memoryCachePolicy(CachePolicy.DISABLED)
.skipMemoryCache(true) diskCachePolicy(CachePolicy.DISABLED)
.diskCacheStrategy(DiskCacheStrategy.NONE) target(GifViewTarget(this@setImage, progressBar, decodeErrorLayout))
.transition(DrawableTransitionOptions.with(NoTransition.getFactory()))
.listener(object : RequestListener<Drawable> {
override fun onLoadFailed(
e: GlideException?,
model: Any?,
target: Target<Drawable>?,
isFirstResource: Boolean
): Boolean {
onImageDecodeError()
return false
} }
override fun onResourceReady(
resource: Drawable?,
model: Any?,
target: Target<Drawable>?,
dataSource: DataSource?,
isFirstResource: Boolean
): Boolean {
if (resource is GifDrawable) {
resource.setLoopCount(GifDrawable.LOOP_INTRINSIC)
}
onImageDecoded()
return false
}
})
.into(this)
} }
companion object { companion object {

View File

@ -4,7 +4,6 @@ import android.annotation.SuppressLint
import android.content.Intent import android.content.Intent
import android.content.res.Resources import android.content.res.Resources
import android.graphics.Color import android.graphics.Color
import android.graphics.drawable.Drawable
import android.net.Uri import android.net.Uri
import android.view.Gravity import android.view.Gravity
import android.view.ViewGroup import android.view.ViewGroup
@ -16,18 +15,12 @@ import android.widget.LinearLayout
import android.widget.TextView import android.widget.TextView
import androidx.appcompat.widget.AppCompatButton import androidx.appcompat.widget.AppCompatButton
import androidx.appcompat.widget.AppCompatImageView import androidx.appcompat.widget.AppCompatImageView
import com.bumptech.glide.load.DataSource import coil.api.clear
import com.bumptech.glide.load.engine.DiskCacheStrategy import coil.api.loadAny
import com.bumptech.glide.load.engine.GlideException import coil.request.CachePolicy
import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions
import com.bumptech.glide.load.resource.gif.GifDrawable
import com.bumptech.glide.request.RequestListener
import com.bumptech.glide.request.target.Target
import com.bumptech.glide.request.transition.NoTransition
import com.davemorrissey.labs.subscaleview.ImageSource import com.davemorrissey.labs.subscaleview.ImageSource
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.glide.GlideApp
import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
import eu.kanade.tachiyomi.ui.reader.viewer.ReaderProgressBar import eu.kanade.tachiyomi.ui.reader.viewer.ReaderProgressBar
@ -36,6 +29,7 @@ import eu.kanade.tachiyomi.util.system.dpToPx
import eu.kanade.tachiyomi.util.view.gone import eu.kanade.tachiyomi.util.view.gone
import eu.kanade.tachiyomi.util.view.updatePaddingRelative import eu.kanade.tachiyomi.util.view.updatePaddingRelative
import eu.kanade.tachiyomi.util.view.visible import eu.kanade.tachiyomi.util.view.visible
import eu.kanade.tachiyomi.widget.GifViewTarget
import rx.Observable import rx.Observable
import rx.Subscription import rx.Subscription
import rx.android.schedulers.AndroidSchedulers import rx.android.schedulers.AndroidSchedulers
@ -149,7 +143,7 @@ class WebtoonPageHolder(
removeDecodeErrorLayout() removeDecodeErrorLayout()
subsamplingImageView?.recycle() subsamplingImageView?.recycle()
subsamplingImageView?.gone() subsamplingImageView?.gone()
imageView?.let { GlideApp.with(frame).clear(it) } imageView?.clear()
imageView?.gone() imageView?.gone()
progressBar.setProgress(0) progressBar.setProgress(0)
} }
@ -491,36 +485,10 @@ class WebtoonPageHolder(
* 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) {
GlideApp.with(this) this.loadAny(stream.readBytes()) {
.load(stream) memoryCachePolicy(CachePolicy.DISABLED)
.skipMemoryCache(true) diskCachePolicy(CachePolicy.DISABLED)
.diskCacheStrategy(DiskCacheStrategy.NONE) target(GifViewTarget(this@setImage, progressBar, decodeErrorLayout))
.transition(DrawableTransitionOptions.with(NoTransition.getFactory()))
.listener(object : RequestListener<Drawable> {
override fun onLoadFailed(
e: GlideException?,
model: Any?,
target: Target<Drawable>?,
isFirstResource: Boolean
): Boolean {
onImageDecodeError()
return false
} }
override fun onResourceReady(
resource: Drawable?,
model: Any?,
target: Target<Drawable>?,
dataSource: DataSource?,
isFirstResource: Boolean
): Boolean {
if (resource is GifDrawable) {
resource.setLoopCount(GifDrawable.LOOP_INTRINSIC)
}
onImageDecoded()
return false
}
})
.into(this)
} }
} }

View File

@ -3,9 +3,10 @@ package eu.kanade.tachiyomi.ui.recent_updates
import android.app.Activity import android.app.Activity
import android.view.View import android.view.View
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import com.bumptech.glide.load.engine.DiskCacheStrategy import coil.api.clear
import coil.api.loadAny
import coil.transform.CircleCropTransformation
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.glide.GlideApp
import eu.kanade.tachiyomi.ui.manga.chapter.BaseChapterHolder import eu.kanade.tachiyomi.ui.manga.chapter.BaseChapterHolder
import eu.kanade.tachiyomi.util.chapter.ChapterUtil import eu.kanade.tachiyomi.util.chapter.ChapterUtil
import eu.kanade.tachiyomi.util.system.getResourceColor import eu.kanade.tachiyomi.util.system.getResourceColor
@ -71,10 +72,9 @@ class RecentChapterHolder(private val view: View, private val adapter: RecentCha
// Set cover // Set cover
if ((view.context as? Activity)?.isDestroyed != true) { if ((view.context as? Activity)?.isDestroyed != true) {
GlideApp.with(itemView.context).clear(manga_cover) manga_cover.clear()
if (!item.manga.thumbnail_url.isNullOrEmpty()) { manga_cover.loadAny(item.manga) {
GlideApp.with(itemView.context).load(item.manga) transformations(CircleCropTransformation())
.diskCacheStrategy(DiskCacheStrategy.RESOURCE).circleCrop().into(manga_cover)
} }
} }

View File

@ -1,10 +1,10 @@
package eu.kanade.tachiyomi.ui.recently_read package eu.kanade.tachiyomi.ui.recently_read
import android.view.View import android.view.View
import com.bumptech.glide.load.engine.DiskCacheStrategy import coil.api.clear
import coil.api.loadAny
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.MangaChapterHistory import eu.kanade.tachiyomi.data.database.models.MangaChapterHistory
import eu.kanade.tachiyomi.data.glide.GlideApp
import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder
import eu.kanade.tachiyomi.util.lang.toTimestampString import eu.kanade.tachiyomi.util.lang.toTimestampString
import kotlinx.android.synthetic.main.recently_read_item.* import kotlinx.android.synthetic.main.recently_read_item.*
@ -59,13 +59,7 @@ class RecentlyReadHolder(
last_read.text = Date(history.last_read).toTimestampString(adapter.dateFormat) last_read.text = Date(history.last_read).toTimestampString(adapter.dateFormat)
// Set cover // Set cover
GlideApp.with(itemView.context).clear(cover) cover.clear()
if (!manga.thumbnail_url.isNullOrEmpty()) { cover.loadAny(manga)
GlideApp.with(itemView.context)
.load(manga)
.diskCacheStrategy(DiskCacheStrategy.RESOURCE)
.centerCrop()
.into(cover)
}
} }
} }

View File

@ -2,12 +2,9 @@ package eu.kanade.tachiyomi.ui.recents
import android.app.Activity import android.app.Activity
import android.view.View import android.view.View
import com.bumptech.glide.load.engine.DiskCacheStrategy import coil.api.loadAny
import com.bumptech.glide.signature.ObjectKey
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.MangaImpl
import eu.kanade.tachiyomi.data.download.model.Download import eu.kanade.tachiyomi.data.download.model.Download
import eu.kanade.tachiyomi.data.glide.GlideApp
import eu.kanade.tachiyomi.source.LocalSource import eu.kanade.tachiyomi.source.LocalSource
import eu.kanade.tachiyomi.ui.manga.chapter.BaseChapterHolder import eu.kanade.tachiyomi.ui.manga.chapter.BaseChapterHolder
import eu.kanade.tachiyomi.util.chapter.ChapterUtil import eu.kanade.tachiyomi.util.chapter.ChapterUtil
@ -72,10 +69,7 @@ class RecentMangaHolder(
) )
} }
if ((itemView.context as? Activity)?.isDestroyed != true) { if ((itemView.context as? Activity)?.isDestroyed != true) {
GlideApp.with(itemView.context).load(item.mch.manga).diskCacheStrategy( cover_thumbnail.loadAny(item.mch.manga)
DiskCacheStrategy.RESOURCE
).signature(ObjectKey(MangaImpl.getLastCoverFetch(item.mch.manga.id!!).toString()))
.into(cover_thumbnail)
} }
notifyStatus( notifyStatus(
if (adapter.isSelected(adapterPosition)) Download.CHECKED else item.status, if (adapter.isSelected(adapterPosition)) Download.CHECKED else item.status,

View File

@ -63,6 +63,16 @@ class SettingsAdvancedController : SettingsController() {
onClick { clearChapterCache() } onClick { clearChapterCache() }
} }
preference {
titleRes = R.string.clean_up_cached_covers
summary = context.getString(R.string.delete_old_covers_in_library_used_, coverCache.getChapterCacheSize())
onClick {
context.toast(R.string.starting_cleanup)
coverCache.deleteOldCovers()
}
}
preference { preference {
titleRes = R.string.clear_cookies titleRes = R.string.clear_cookies
@ -101,16 +111,6 @@ class SettingsAdvancedController : SettingsController() {
onClick { cleanupDownloads() } onClick { cleanupDownloads() }
} }
preference {
titleRes = R.string.clean_up_cached_covers
summaryRes = R.string.delete_old_covers_in_library
onClick {
context.toast(R.string.starting_cleanup)
coverCache.deleteOldCovers()
}
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
val pm = context.getSystemService(Context.POWER_SERVICE) as? PowerManager? val pm = context.getSystemService(Context.POWER_SERVICE) as? PowerManager?
if (pm != null) preference { if (pm != null) preference {

View File

@ -3,17 +3,18 @@ package eu.kanade.tachiyomi.ui.source.browse
import android.app.Activity import android.app.Activity
import android.view.View import android.view.View
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.Glide import coil.Coil
import com.bumptech.glide.load.engine.DiskCacheStrategy import coil.api.clear
import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions import coil.request.LoadRequest
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.glide.GlideApp
import eu.kanade.tachiyomi.ui.library.LibraryCategoryAdapter import eu.kanade.tachiyomi.ui.library.LibraryCategoryAdapter
import eu.kanade.tachiyomi.util.view.gone import eu.kanade.tachiyomi.util.view.gone
import eu.kanade.tachiyomi.widget.StateImageViewTarget import eu.kanade.tachiyomi.widget.CoverViewTarget
import kotlinx.android.synthetic.main.manga_grid_item.* import kotlinx.android.synthetic.main.manga_grid_item.*
import kotlinx.android.synthetic.main.manga_grid_item.cover_thumbnail
import kotlinx.android.synthetic.main.manga_grid_item.title
import kotlinx.android.synthetic.main.unread_download_badge.* import kotlinx.android.synthetic.main.unread_download_badge.*
/** /**
@ -58,16 +59,12 @@ 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) {
Glide.with(view.context).clear(cover_thumbnail) cover_thumbnail.clear()
else { } else {
GlideApp.with(view.context) val id = manga.id ?: return
.load(manga) val request = LoadRequest.Builder(view.context).data(manga).target(CoverViewTarget(cover_thumbnail, progress)).build()
.diskCacheStrategy(DiskCacheStrategy.DATA) Coil.imageLoader(view.context).execute(request)
.centerCrop()
.placeholder(android.R.color.transparent)
.transition(DrawableTransitionOptions.withCrossFade())
.into(StateImageViewTarget(cover_thumbnail, progress))
} }
} }
} }

View File

@ -2,15 +2,16 @@ package eu.kanade.tachiyomi.ui.source.browse
import android.view.View import android.view.View
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.load.engine.DiskCacheStrategy import coil.Coil
import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions import coil.api.clear
import coil.request.LoadRequest
import coil.transform.RoundedCornersTransformation
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.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.glide.GlideApp
import eu.kanade.tachiyomi.util.system.getResourceColor import eu.kanade.tachiyomi.util.system.getResourceColor
import eu.kanade.tachiyomi.widget.StateImageViewTarget import eu.kanade.tachiyomi.widget.CoverViewTarget
import kotlinx.android.synthetic.main.manga_list_item.* import kotlinx.android.synthetic.main.manga_list_item.*
/** /**
@ -42,17 +43,14 @@ class BrowseSourceListHolder(private val view: View, adapter: FlexibleAdapter<IF
} }
override fun setImage(manga: Manga) { override fun setImage(manga: Manga) {
if (manga.thumbnail_url.isNullOrEmpty()) { // Update the cover.
GlideApp.with(view.context).clear(contentView) if (manga.thumbnail_url == null) {
cover_thumbnail.clear()
} else { } else {
GlideApp.with(view.context) val id = manga.id ?: return
.load(manga) val request = LoadRequest.Builder(view.context).data(manga).target(CoverViewTarget(cover_thumbnail))
.diskCacheStrategy(DiskCacheStrategy.DATA) .transformations(RoundedCornersTransformation(2f, 2f, 2f, 2f)).build()
.dontAnimate() Coil.imageLoader(view.context).execute(request)
.centerCrop()
.placeholder(android.R.color.transparent)
.transition(DrawableTransitionOptions.withCrossFade())
.into(StateImageViewTarget(cover_thumbnail, progress))
} }
} }
} }

View File

@ -284,7 +284,7 @@ open class BrowseSourcePresenter(
} }
fun confirmDeletion(manga: Manga) { fun confirmDeletion(manga: Manga) {
coverCache.deleteFromCache(manga.thumbnail_url) coverCache.deleteFromCache(manga)
val downloadManager: DownloadManager = Injekt.get() val downloadManager: DownloadManager = Injekt.get()
downloadManager.deleteManga(manga, source) downloadManager.deleteManga(manga, source)
db.resetMangaInfo(manga).executeAsBlocking() db.resetMangaInfo(manga).executeAsBlocking()

View File

@ -1,12 +1,14 @@
package eu.kanade.tachiyomi.ui.source.global_search package eu.kanade.tachiyomi.ui.source.global_search
import android.view.View import android.view.View
import com.bumptech.glide.load.engine.DiskCacheStrategy import coil.Coil
import coil.api.clear
import coil.request.CachePolicy
import coil.request.LoadRequest
import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.glide.GlideApp
import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder
import eu.kanade.tachiyomi.util.view.visibleIf import eu.kanade.tachiyomi.util.view.visibleIf
import eu.kanade.tachiyomi.widget.StateImageViewTarget import eu.kanade.tachiyomi.widget.CoverViewTarget
import kotlinx.android.synthetic.main.source_global_search_controller_card_item.* import kotlinx.android.synthetic.main.source_global_search_controller_card_item.*
class GlobalSearchMangaHolder(view: View, adapter: GlobalSearchCardAdapter) : class GlobalSearchMangaHolder(view: View, adapter: GlobalSearchCardAdapter) :
@ -36,15 +38,13 @@ class GlobalSearchMangaHolder(view: View, adapter: GlobalSearchCardAdapter) :
} }
fun setImage(manga: Manga) { fun setImage(manga: Manga) {
GlideApp.with(itemView.context).clear(itemImage) itemImage.clear()
if (!manga.thumbnail_url.isNullOrEmpty()) { if (!manga.thumbnail_url.isNullOrEmpty()) {
GlideApp.with(itemView.context) val request = LoadRequest.Builder(itemView.context).data(manga)
.load(manga)
.diskCacheStrategy(DiskCacheStrategy.DATA)
.centerCrop()
.skipMemoryCache(true)
.placeholder(android.R.color.transparent) .placeholder(android.R.color.transparent)
.into(StateImageViewTarget(itemImage, progress)) .memoryCachePolicy(CachePolicy.DISABLED)
.target(CoverViewTarget(itemImage, progress)).build()
Coil.imageLoader(itemView.context).execute(request)
} }
} }
} }

View File

@ -0,0 +1,32 @@
package eu.kanade.tachiyomi.widget
import android.graphics.drawable.Drawable
import android.view.View
import android.widget.ImageView
import androidx.vectordrawable.graphics.drawable.VectorDrawableCompat
import coil.target.ImageViewTarget
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.util.system.getResourceColor
import eu.kanade.tachiyomi.util.view.gone
import eu.kanade.tachiyomi.util.view.visible
class CoverViewTarget(view: ImageView, val progress: View? = null) : ImageViewTarget(view) {
override fun onError(error: Drawable?) {
progress?.gone()
view.scaleType = ImageView.ScaleType.CENTER
val vector = VectorDrawableCompat.create(view.context.resources, R.drawable.ic_broken_image_grey_24dp, null)
vector?.setTint(view.context.getResourceColor(android.R.attr.textColorSecondary))
view.setImageDrawable(vector)
}
override fun onStart(placeholder: Drawable?) {
progress?.visible()
super.onStart(placeholder)
}
override fun onSuccess(result: Drawable) {
progress?.gone()
super.onSuccess(result)
}
}

View File

@ -0,0 +1,23 @@
package eu.kanade.tachiyomi.widget
import android.graphics.drawable.Drawable
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import coil.target.ImageViewTarget
import eu.kanade.tachiyomi.util.view.gone
import eu.kanade.tachiyomi.util.view.visible
class GifViewTarget(view: ImageView, private val progressBar: View?, private val decodeErrorLayout: ViewGroup?) : ImageViewTarget(view) {
override fun onError(error: Drawable?) {
progressBar?.gone()
decodeErrorLayout?.visible()
}
override fun onSuccess(result: Drawable) {
progressBar?.gone()
decodeErrorLayout?.gone()
super.onSuccess(result)
}
}

View File

@ -1,66 +0,0 @@
package eu.kanade.tachiyomi.widget
import android.graphics.drawable.Drawable
import android.view.View
import android.widget.ImageView
import android.widget.ImageView.ScaleType
import androidx.vectordrawable.graphics.drawable.VectorDrawableCompat
import com.bumptech.glide.request.target.ImageViewTarget
import com.bumptech.glide.request.transition.Transition
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.util.system.getResourceColor
import eu.kanade.tachiyomi.util.view.gone
import eu.kanade.tachiyomi.util.view.visible
/**
* A glide target to display an image with an optional view to show while loading and a configurable
* error drawable.
*
* @param view the view where the image will be loaded
* @param progress an optional view to show when the image is loading.
* @param errorDrawableRes the error drawable resource to show.
* @param errorScaleType the scale type for the error drawable, [ScaleType.CENTER] by default.
*/
class StateImageViewTarget(
view: ImageView,
val progress: View? = null,
val errorDrawableRes: Int = R.drawable.ic_broken_image_grey_24dp,
val errorScaleType: ScaleType = ScaleType.CENTER
) :
ImageViewTarget<Drawable>(view) {
private var resource: Drawable? = null
private val imageScaleType = view.scaleType
override fun setResource(resource: Drawable?) {
view.setImageDrawable(resource)
}
override fun onLoadStarted(placeholder: Drawable?) {
progress?.visible()
super.onLoadStarted(placeholder)
}
override fun onLoadFailed(errorDrawable: Drawable?) {
progress?.gone()
view.scaleType = errorScaleType
val vector = VectorDrawableCompat.create(view.context.resources, errorDrawableRes, null)
vector?.setTint(view.context.getResourceColor(android.R.attr.textColorSecondary))
view.setImageDrawable(vector)
}
override fun onLoadCleared(placeholder: Drawable?) {
progress?.gone()
super.onLoadCleared(placeholder)
}
override fun onResourceReady(resource: Drawable, transition: Transition<in Drawable>?) {
progress?.gone()
view.scaleType = imageScaleType
super.onResourceReady(resource, transition)
this.resource = resource
}
}

View File

@ -34,9 +34,8 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:adjustViewBounds="true" android:adjustViewBounds="true"
android:scaleType="centerCrop"
android:background="?android:attr/colorBackground" android:background="?android:attr/colorBackground"
android:maxHeight="250dp"
tools:background="?android:attr/colorBackground"
tools:ignore="ContentDescription" tools:ignore="ContentDescription"
tools:src="@mipmap/ic_launcher" /> tools:src="@mipmap/ic_launcher" />

View File

@ -540,8 +540,8 @@
<item quantity="other">Cleanup done. Removed %d folders</item> <item quantity="other">Cleanup done. Removed %d folders</item>
</plurals> </plurals>
<string name="clean_up_cached_covers">Clean up cached covers</string> <string name="clean_up_cached_covers">Clean up cached covers</string>
<string name="delete_old_covers_in_library">Delete old and unused cached covers of <string name="delete_old_covers_in_library_used_">Delete old and unused cached covers of
manga in your library that has been updated</string> manga in your library that has been updated.\nCurrently using: %1$s</string>
<!-- About section --> <!-- About section -->
<string name="version">Version</string> <string name="version">Version</string>