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 {
setSourceCompatibility(1.8)
setTargetCompatibility(1.8)
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = "1.8"
@ -141,6 +141,21 @@ dependencies {
debugImplementation ("com.github.ChuckerTeam.Chucker:library:$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
val retrofitVersion = "2.7.2"
implementation("com.squareup.retrofit2:retrofit:$retrofitVersion")
@ -184,10 +199,10 @@ dependencies {
implementation("com.github.inorichi.injekt:injekt-core:65b0440")
// Image library
val glideVersion = "4.11.0"
implementation("com.github.bumptech.glide:glide:$glideVersion")
implementation("com.github.bumptech.glide:okhttp3-integration:$glideVersion")
kapt("com.github.bumptech.glide:compiler:$glideVersion")
val coilVersion = "0.10.1"
implementation("io.coil-kt:coil:$coilVersion")
implementation("io.coil-kt:coil-gif:$coilVersion")
implementation("io.coil-kt:coil-svg:$coilVersion")
// Logging
implementation("com.jakewharton.timber:timber:4.7.1")

View File

@ -27,14 +27,6 @@
-dontwarn javax.annotation.**
-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
-dontwarn sun.misc.**

View File

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

View File

@ -2,8 +2,10 @@ package eu.kanade.tachiyomi.data.cache
import android.content.Context
import android.text.format.Formatter
import coil.Coil
import eu.kanade.tachiyomi.R
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.system.executeOnIO
import eu.kanade.tachiyomi.util.system.toast
@ -11,6 +13,7 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import okhttp3.Cache
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.io.File
@ -20,13 +23,12 @@ import java.io.InputStream
/**
* Class used to create cover cache.
* 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.
*
* @param context the application context.
* @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.
@ -34,14 +36,18 @@ class CoverCache(private val context: Context) {
private val cacheDir = context.getExternalFilesDir("covers")
?: 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() {
GlobalScope.launch(Dispatchers.Default) {
val db = Injekt.get<DatabaseHelper>()
var deletedSize = 0L
val urls = db.getLibraryMangas().executeOnIO().mapNotNull {
it.thumbnail_url?.let { url ->
return@mapNotNull DiskUtil.hashKeyForDisk(url)
}
it.thumbnail_url?.let { url -> return@mapNotNull it.key() }
null
}
val files = cacheDir.listFiles()?.iterator() ?: return@launch
@ -68,8 +74,8 @@ class CoverCache(private val context: Context) {
* @param thumbnailUrl the thumbnail url.
* @return cover image.
*/
fun getCoverFile(thumbnailUrl: String): File {
return File(cacheDir, DiskUtil.hashKeyForDisk(thumbnailUrl))
fun getCoverFile(manga: Manga): File {
return File(cacheDir, manga.key())
}
/**
@ -80,26 +86,26 @@ class CoverCache(private val context: Context) {
* @throws IOException if there's any error.
*/
@Throws(IOException::class)
fun copyToCache(thumbnailUrl: String, inputStream: InputStream) {
fun copyToCache(manga: Manga, inputStream: InputStream) {
// Get destination file.
val destFile = getCoverFile(thumbnailUrl)
val destFile = getCoverFile(manga)
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.
* @return status of deletion.
*/
fun deleteFromCache(thumbnailUrl: String?): Boolean {
fun deleteFromCache(manga: Manga, deleteMemoryCache: Boolean = true) {
// Check if url is empty.
if (thumbnailUrl.isNullOrEmpty())
return false
if (manga.thumbnail_url.isNullOrEmpty()) return
// Remove file.
val file = getCoverFile(thumbnailUrl)
return file.exists() && file.delete()
// Remove file
val file = getCoverFile(manga)
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.model.SManga
import eu.kanade.tachiyomi.ui.reader.ReaderActivity
import eu.kanade.tachiyomi.util.storage.DiskUtil
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.util.Locale
import kotlin.random.Random
interface Manga : SManga {
@ -52,13 +54,15 @@ interface Manga : SManga {
fun showChapterTitle(defaultShow: Boolean): Boolean = chapter_flags and DISPLAY_MASK == DISPLAY_NUMBER
fun mangaType(context: Context): String {
return context.getString(when (mangaType()) {
return context.getString(
when (mangaType()) {
TYPE_WEBTOON -> R.string.webtoon
TYPE_MANHWA -> R.string.manhwa
TYPE_MANHUA -> R.string.manhua
TYPE_COMIC -> R.string.comic
else -> R.string.manga
}).toLowerCase(Locale.getDefault())
}
).toLowerCase(Locale.getDefault())
}
/**
@ -97,14 +101,16 @@ interface Manga : SManga {
return if (currentTags?.any
{ tag ->
tag == "long strip" || tag == "manhwa" || tag.contains("webtoon")
} == true || isWebtoonSource(sourceName))
} == true || isWebtoonSource(sourceName)
)
ReaderActivity.WEBTOON
else if (currentTags?.any
{ tag ->
tag == "chinese" || tag == "manhua" ||
tag.startsWith("english") || tag == "comic"
} == true || (isComicSource(sourceName) && !sourceName.contains("tapastic", true)) ||
sourceName.contains("manhua", true))
sourceName.contains("manhua", true)
)
ReaderActivity.LEFT_TO_RIGHT
else 0
}
@ -139,6 +145,19 @@ interface Manga : SManga {
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
var displayMode: Int
get() = chapter_flags and DISPLAY_MASK

View File

@ -67,14 +67,4 @@ open class MangaImpl : Manga {
if (::url.isInitialized) return url.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.Intent
import android.graphics.BitmapFactory
import android.graphics.drawable.BitmapDrawable
import android.os.Build
import android.os.IBinder
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.NotificationManagerCompat
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.data.database.DatabaseHelper
import eu.kanade.tachiyomi.data.database.models.Category
import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.data.database.models.LibraryManga
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.database.models.MangaImpl
import eu.kanade.tachiyomi.data.download.DownloadManager
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.LibraryUpdateService.Companion.start
import eu.kanade.tachiyomi.data.notification.NotificationReceiver
@ -56,7 +59,6 @@ import timber.log.Timber
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.util.ArrayList
import java.util.Date
import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicInteger
@ -530,10 +532,16 @@ class LibraryUpdateService(
val thumbnailUrl = manga.thumbnail_url
manga.copyFrom(networkManga)
manga.initialized = true
db.insertManga(manga).executeAsBlocking()
if (thumbnailUrl != networkManga.thumbnail_url && !manga.hasCustomCover()) {
MangaImpl.setLastCoverFetch(manga.id!!, Date().time)
// load new covers in background
if (!manga.hasCustomCover()) {
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) {
setSmallIcon(R.drawable.ic_tachi)
try {
val icon = GlideApp.with(this@LibraryUpdateService)
.asBitmap().load(manga).dontTransform().centerCrop().circleCrop()
.override(256, 256).submit().get()
setLargeIcon(icon)
val request = LoadRequest.Builder(this@LibraryUpdateService).data(manga)
.transformations(CircleCropTransformation()).size(width = 256, height = 256)
.target { drawable -> setLargeIcon((drawable as BitmapDrawable).bitmap) }.build()
Coil.imageLoader(this@LibraryUpdateService).execute(request)
} catch (e: Exception) {
}
setGroupAlertBehavior(GROUP_ALERT_SUMMARY)

View File

@ -47,8 +47,8 @@ class LocalSource(private val context: Context) : CatalogueSource {
return null
}
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()
input.use {
cover.outputStream().use {

View File

@ -4,15 +4,18 @@ import android.content.res.ColorStateList
import android.graphics.Color
import android.view.View
import androidx.core.content.ContextCompat
import coil.api.clear
import coil.api.load
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.InstallStep
import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder
import eu.kanade.tachiyomi.util.system.LocaleHelper
import eu.kanade.tachiyomi.util.system.getResourceColor
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.source_global_search_controller_card_item.*
class ExtensionHolder(view: View, val adapter: ExtensionAdapter) :
BaseFlexibleViewHolder(view, adapter) {
@ -35,11 +38,12 @@ class ExtensionHolder(view: View, val adapter: ExtensionAdapter) :
itemView.context.getString(R.string.untrusted).toUpperCase()
}
GlideApp.with(itemView.context).clear(edit_button)
edit_button.clear()
if (extension is Extension.Available) {
GlideApp.with(itemView.context)
.load(extension.iconUrl)
.into(edit_button)
edit_button.load(extension.iconUrl) {
target(CoverViewTarget(edit_button, progress))
}
} else {
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.View
import android.widget.FrameLayout
import com.bumptech.glide.load.engine.DiskCacheStrategy
import com.bumptech.glide.signature.ObjectKey
import coil.api.clear
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.MangaImpl
import eu.kanade.tachiyomi.data.glide.GlideApp
import eu.kanade.tachiyomi.util.view.gone
import eu.kanade.tachiyomi.util.view.visibleIf
import kotlinx.android.synthetic.main.manga_grid_item.*
@ -65,29 +65,26 @@ class LibraryGridHolder(
setReadingButton(item)
// 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 {
val id = item.manga.id ?: return
if (cover_thumbnail.height == 0) {
val oldPos = adapterPosition
adapter.recyclerView.post {
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
GlideApp.with(adapter.recyclerView.context).load(manga)
.diskCacheStrategy(DiskCacheStrategy.RESOURCE)
.signature(ObjectKey(MangaImpl.getLastCoverFetch(id).toString()))
.apply {
if (fixedSize) centerCrop()
else override(cover_thumbnail.maxHeight)
cover_thumbnail.loadAny(manga) {
if (!fixedSize) {
precision(Precision.INEXACT)
scale(Scale.FIT)
}
}
.into(cover_thumbnail)
}
private fun playButtonClicked() {

View File

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

View File

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

View File

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

View File

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

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.Manga
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.download.DownloadManager
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.fetchMangaDetailsAsync
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.track.TrackItem
import eu.kanade.tachiyomi.ui.security.SecureActivityDelegate
@ -368,6 +368,12 @@ class MangaDetailsPresenter(
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) */
fun refreshAll() {
if (controller.isNotOnline() && manga.source != LocalSource.ID) return
@ -398,13 +404,16 @@ class MangaDetailsPresenter(
if (networkManga != null) {
manga.copyFrom(networkManga)
manga.initialized = true
db.insertManga(manga).executeAsBlocking()
if (thumbnailUrl != networkManga.thumbnail_url && !manga.hasCustomCover()) {
coverCache.deleteFromCache(thumbnailUrl)
MangaImpl.setLastCoverFetch(manga.id!!, Date().time)
withContext(Dispatchers.Main) { controller.setPaletteColor() }
if (shouldUpdateCover(thumbnailUrl, networkManga)) {
coverCache.deleteFromCache(manga, false)
manga.thumbnail_url = networkManga.thumbnail_url
withContext(Dispatchers.Main) {
forceUpdateCovers()
}
}
db.insertManga(manga).executeAsBlocking()
}
val finChapters = chapters.await()
if (finChapters.isNotEmpty()) {
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.
*/
@ -588,7 +610,9 @@ class MangaDetailsPresenter(
manga.favorite = !manga.favorite
when (manga.favorite) {
true -> manga.date_added = Date().time
true -> {
manga.date_added = Date().time
}
false -> manga.date_added = 0
}
@ -639,7 +663,7 @@ class MangaDetailsPresenter(
}
fun confirmDeletion() {
coverCache.deleteFromCache(manga.thumbnail_url)
coverCache.deleteFromCache(manga)
db.resetMangaInfo(manga).executeAsBlocking()
downloadManager.deleteManga(manga, source)
asyncUpdateMangaAndChapters(true)
@ -707,36 +731,41 @@ class MangaDetailsPresenter(
db.updateMangaInfo(manga).executeAsBlocking()
}
if (uri != null) editCoverWithStream(uri)
controller.updateHeader()
}
fun clearCover() {
/**
* Remvoe custom cover
*/
fun clearCustomCover() {
if (manga.hasCustomCover()) {
coverCache.deleteFromCache(manga.thumbnail_url!!)
manga.thumbnail_url = manga.thumbnail_url?.removePrefix("Custom-")
coverCache.deleteFromCache(manga)
manga.removeCustomThumbnailUrl()
db.insertManga(manga).executeAsBlocking()
MangaImpl.setLastCoverFetch(manga.id!!, Date().time)
controller.updateHeader()
controller.setPaletteColor()
forceUpdateCovers()
}
}
fun forceUpdateCovers(deleteCache: Boolean = true) {
if (deleteCache) coverCache.deleteFromCache(manga)
controller.setPaletteColor()
controller.resetCovers()
}
fun editCoverWithStream(uri: Uri): Boolean {
val inputStream =
downloadManager.context.contentResolver.openInputStream(uri) ?: return false
if (manga.source == LocalSource.ID) {
LocalSource.updateCover(downloadManager.context, manga, inputStream)
MangaImpl.setLastCoverFetch(manga.id!!, Date().time)
forceUpdateCovers()
return true
}
if (manga.favorite) {
if (!manga.hasCustomCover()) {
manga.thumbnail_url = "Custom-${manga.thumbnail_url ?: manga.id!!}"
coverCache.deleteFromCache(manga)
manga.setCustomThumbnailUrl()
db.insertManga(manga).executeAsBlocking()
}
coverCache.copyToCache(manga.thumbnail_url!!, inputStream)
MangaImpl.setLastCoverFetch(manga.id!!, Date().time)
coverCache.copyToCache(manga, inputStream)
forceUpdateCovers(false)
return true
}
return false

View File

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

View File

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

View File

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

View File

@ -3,12 +3,11 @@ package eu.kanade.tachiyomi.ui.migration.manga.process
import android.view.View
import androidx.appcompat.widget.PopupMenu
import androidx.constraintlayout.widget.ConstraintLayout
import com.bumptech.glide.load.engine.DiskCacheStrategy
import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions
import coil.Coil
import coil.request.LoadRequest
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.glide.GlideApp
import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.SourceManager
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.visible
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.migration_process_item.*
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) {
(layoutParams as ConstraintLayout.LayoutParams).verticalBias = 1f
progress.gone()
GlideApp.with(view.context.applicationContext).load(manga).apply {
diskCacheStrategy(DiskCacheStrategy.RESOURCE)
if (isTo) {
transition(DrawableTransitionOptions.withCrossFade())
.into(StateImageViewTarget(cover_thumbnail, progress))
} else
into(cover_thumbnail)
}
val request = LoadRequest.Builder(view.context).data(manga)
.target(CoverViewTarget(cover_thumbnail, progress)).build()
Coil.imageLoader(view.context).execute(request)
compact_title.visible()
gradient.visible()
@ -164,11 +159,15 @@ class MigrationProcessHolder(
val latestChapter = mangaChapters.maxBy { it.chapter_number }?.chapter_number ?: -1f
if (latestChapter > 0f) {
subtitle.text = context.getString(R.string.latest_,
DecimalFormat("#.#").format(latestChapter))
subtitle.text = context.getString(
R.string.latest_,
DecimalFormat("#.#").format(latestChapter)
)
} else {
subtitle.text = context.getString(R.string.latest_,
context.getString(R.string.unknown))
subtitle.text = context.getString(
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.History
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.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.preference.getOrDefault
@ -536,18 +535,17 @@ class ReaderPresenter(
.fromCallable {
if (manga.source == LocalSource.ID) {
val context = Injekt.get<Application>()
coverCache.deleteFromCache(manga)
LocalSource.updateCover(context, manga, stream())
R.string.cover_updated
SetAsCoverResult.Success
} 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.hasCustomCover()) {
manga.thumbnail_url = "Custom-${manga.thumbnail_url ?: manga.id!!}"
coverCache.deleteFromCache(manga)
manga.setCustomThumbnailUrl()
db.insertManga(manga).executeAsBlocking()
}
coverCache.copyToCache(manga.thumbnail_url!!, stream())
MangaImpl.setLastCoverFetch(manga.id!!, Date().time)
coverCache.copyToCache(manga, stream())
SetAsCoverResult.Success
} else {
SetAsCoverResult.AddToLibraryFirst

View File

@ -2,11 +2,13 @@ package eu.kanade.tachiyomi.ui.reader
import android.content.Context
import android.graphics.Bitmap
import android.graphics.drawable.BitmapDrawable
import androidx.core.app.NotificationCompat
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.data.glide.GlideApp
import eu.kanade.tachiyomi.data.notification.NotificationHandler
import eu.kanade.tachiyomi.data.notification.NotificationReceiver
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.
*/
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) {
showCompleteNotification(file, bitmap)
} else {
onError(null)
}
}).build()
Coil.imageLoader(context).execute(request)
}
private fun showCompleteNotification(file: File, image: Bitmap) {

View File

@ -18,19 +18,12 @@ import android.widget.FrameLayout
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.TextView
import com.bumptech.glide.load.DataSource
import com.bumptech.glide.load.engine.DiskCacheStrategy
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 coil.api.loadAny
import coil.request.CachePolicy
import com.davemorrissey.labs.subscaleview.ImageSource
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
import com.github.chrisbanes.photoview.PhotoView
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.glide.GlideApp
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.source.model.Page
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.view.gone
import eu.kanade.tachiyomi.util.view.visible
import eu.kanade.tachiyomi.widget.GifViewTarget
import eu.kanade.tachiyomi.widget.ViewPagerAdapter
import kotlinx.coroutines.Dispatchers.Default
import kotlinx.coroutines.withContext
@ -487,37 +481,11 @@ class PagerPageHolder(
* Extension method to set a [stream] into this ImageView.
*/
private fun ImageView.setImage(stream: InputStream) {
GlideApp.with(this)
.load(stream)
.skipMemoryCache(true)
.diskCacheStrategy(DiskCacheStrategy.NONE)
.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
this.loadAny(stream.readBytes()) {
memoryCachePolicy(CachePolicy.DISABLED)
diskCachePolicy(CachePolicy.DISABLED)
target(GifViewTarget(this@setImage, progressBar, decodeErrorLayout))
}
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 {

View File

@ -4,7 +4,6 @@ import android.annotation.SuppressLint
import android.content.Intent
import android.content.res.Resources
import android.graphics.Color
import android.graphics.drawable.Drawable
import android.net.Uri
import android.view.Gravity
import android.view.ViewGroup
@ -16,18 +15,12 @@ import android.widget.LinearLayout
import android.widget.TextView
import androidx.appcompat.widget.AppCompatButton
import androidx.appcompat.widget.AppCompatImageView
import com.bumptech.glide.load.DataSource
import com.bumptech.glide.load.engine.DiskCacheStrategy
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 coil.api.clear
import coil.api.loadAny
import coil.request.CachePolicy
import com.davemorrissey.labs.subscaleview.ImageSource
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.glide.GlideApp
import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
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.updatePaddingRelative
import eu.kanade.tachiyomi.util.view.visible
import eu.kanade.tachiyomi.widget.GifViewTarget
import rx.Observable
import rx.Subscription
import rx.android.schedulers.AndroidSchedulers
@ -149,7 +143,7 @@ class WebtoonPageHolder(
removeDecodeErrorLayout()
subsamplingImageView?.recycle()
subsamplingImageView?.gone()
imageView?.let { GlideApp.with(frame).clear(it) }
imageView?.clear()
imageView?.gone()
progressBar.setProgress(0)
}
@ -491,36 +485,10 @@ class WebtoonPageHolder(
* Extension method to set a [stream] into this ImageView.
*/
private fun ImageView.setImage(stream: InputStream) {
GlideApp.with(this)
.load(stream)
.skipMemoryCache(true)
.diskCacheStrategy(DiskCacheStrategy.NONE)
.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
this.loadAny(stream.readBytes()) {
memoryCachePolicy(CachePolicy.DISABLED)
diskCachePolicy(CachePolicy.DISABLED)
target(GifViewTarget(this@setImage, progressBar, decodeErrorLayout))
}
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.view.View
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.data.glide.GlideApp
import eu.kanade.tachiyomi.ui.manga.chapter.BaseChapterHolder
import eu.kanade.tachiyomi.util.chapter.ChapterUtil
import eu.kanade.tachiyomi.util.system.getResourceColor
@ -71,10 +72,9 @@ class RecentChapterHolder(private val view: View, private val adapter: RecentCha
// Set cover
if ((view.context as? Activity)?.isDestroyed != true) {
GlideApp.with(itemView.context).clear(manga_cover)
if (!item.manga.thumbnail_url.isNullOrEmpty()) {
GlideApp.with(itemView.context).load(item.manga)
.diskCacheStrategy(DiskCacheStrategy.RESOURCE).circleCrop().into(manga_cover)
manga_cover.clear()
manga_cover.loadAny(item.manga) {
transformations(CircleCropTransformation())
}
}

View File

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

View File

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

View File

@ -63,6 +63,16 @@ class SettingsAdvancedController : SettingsController() {
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 {
titleRes = R.string.clear_cookies
@ -101,16 +111,6 @@ class SettingsAdvancedController : SettingsController() {
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) {
val pm = context.getSystemService(Context.POWER_SERVICE) as? PowerManager?
if (pm != null) preference {

View File

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

View File

@ -2,15 +2,16 @@ package eu.kanade.tachiyomi.ui.source.browse
import android.view.View
import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.load.engine.DiskCacheStrategy
import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions
import coil.Coil
import coil.api.clear
import coil.request.LoadRequest
import coil.transform.RoundedCornersTransformation
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.IFlexible
import eu.kanade.tachiyomi.R
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.widget.StateImageViewTarget
import eu.kanade.tachiyomi.widget.CoverViewTarget
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) {
if (manga.thumbnail_url.isNullOrEmpty()) {
GlideApp.with(view.context).clear(contentView)
// Update the cover.
if (manga.thumbnail_url == null) {
cover_thumbnail.clear()
} else {
GlideApp.with(view.context)
.load(manga)
.diskCacheStrategy(DiskCacheStrategy.DATA)
.dontAnimate()
.centerCrop()
.placeholder(android.R.color.transparent)
.transition(DrawableTransitionOptions.withCrossFade())
.into(StateImageViewTarget(cover_thumbnail, progress))
val id = manga.id ?: return
val request = LoadRequest.Builder(view.context).data(manga).target(CoverViewTarget(cover_thumbnail))
.transformations(RoundedCornersTransformation(2f, 2f, 2f, 2f)).build()
Coil.imageLoader(view.context).execute(request)
}
}
}

View File

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

View File

@ -1,12 +1,14 @@
package eu.kanade.tachiyomi.ui.source.global_search
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.glide.GlideApp
import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder
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.*
class GlobalSearchMangaHolder(view: View, adapter: GlobalSearchCardAdapter) :
@ -36,15 +38,13 @@ class GlobalSearchMangaHolder(view: View, adapter: GlobalSearchCardAdapter) :
}
fun setImage(manga: Manga) {
GlideApp.with(itemView.context).clear(itemImage)
itemImage.clear()
if (!manga.thumbnail_url.isNullOrEmpty()) {
GlideApp.with(itemView.context)
.load(manga)
.diskCacheStrategy(DiskCacheStrategy.DATA)
.centerCrop()
.skipMemoryCache(true)
val request = LoadRequest.Builder(itemView.context).data(manga)
.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_height="wrap_content"
android:adjustViewBounds="true"
android:scaleType="centerCrop"
android:background="?android:attr/colorBackground"
android:maxHeight="250dp"
tools:background="?android:attr/colorBackground"
tools:ignore="ContentDescription"
tools:src="@mipmap/ic_launcher" />

View File

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