* Use Coil

* Remove coil-transformations lib

* Add MangaCoverFetcher

* Remove Glide

* MangaCoverFetcher: Allow skipping custom cover usage

* Adjust coil caching policy for some non-library items

* Allow coil to use RGB565 only on low ram devices

* Fix image loading progress view not showing

a

* Increase coil crossfade duration

Same as default glide duration

* Add back request clearing
This commit is contained in:
Ivan Iskandar 2021-04-28 19:32:00 +07:00 committed by GitHub
parent 7d23fd8ef5
commit 93e6136795
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
39 changed files with 492 additions and 797 deletions

View File

@ -198,10 +198,9 @@ dependencies {
implementation("com.github.inorichi.injekt:injekt-core:65b0440") implementation("com.github.inorichi.injekt:injekt-core:65b0440")
// Image library // Image library
val glideVersion = "4.12.0" val coilVersion = "1.2.0"
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("com.github.tachiyomiorg:subsampling-scale-image-view:547d9c0") implementation("com.github.tachiyomiorg:subsampling-scale-image-view:547d9c0")
@ -278,7 +277,8 @@ tasks {
"-Xuse-experimental=kotlinx.coroutines.FlowPreview", "-Xuse-experimental=kotlinx.coroutines.FlowPreview",
"-Xuse-experimental=kotlinx.coroutines.ExperimentalCoroutinesApi", "-Xuse-experimental=kotlinx.coroutines.ExperimentalCoroutinesApi",
"-Xuse-experimental=kotlinx.coroutines.InternalCoroutinesApi", "-Xuse-experimental=kotlinx.coroutines.InternalCoroutinesApi",
"-Xuse-experimental=kotlinx.serialization.ExperimentalSerializationApi" "-Xuse-experimental=kotlinx.serialization.ExperimentalSerializationApi",
"-Xuse-experimental=coil.annotation.ExperimentalCoilApi",
) )
} }

View File

@ -1,16 +1,25 @@
package eu.kanade.tachiyomi package eu.kanade.tachiyomi
import android.app.ActivityManager
import android.app.Application import android.app.Application
import android.content.Context import android.content.Context
import android.content.res.Configuration import android.content.res.Configuration
import android.os.Build import android.os.Build
import androidx.core.content.getSystemService
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleObserver 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 coil.ImageLoader
import coil.ImageLoaderFactory
import coil.decode.GifDecoder
import coil.decode.ImageDecoderDecoder
import eu.kanade.tachiyomi.data.coil.ByteBufferFetcher
import eu.kanade.tachiyomi.data.coil.MangaCoverFetcher
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.network.NetworkHelper
import eu.kanade.tachiyomi.ui.security.SecureActivityDelegate import eu.kanade.tachiyomi.ui.security.SecureActivityDelegate
import eu.kanade.tachiyomi.util.system.LocaleHelper import eu.kanade.tachiyomi.util.system.LocaleHelper
import org.acra.ACRA import org.acra.ACRA
@ -20,6 +29,7 @@ import org.acra.sender.HttpSender
import org.conscrypt.Conscrypt import org.conscrypt.Conscrypt
import timber.log.Timber import timber.log.Timber
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
import java.security.Security import java.security.Security
@ -31,7 +41,7 @@ import java.security.Security
uri = BuildConfig.ACRA_URI, uri = BuildConfig.ACRA_URI,
httpMethod = HttpSender.Method.PUT httpMethod = HttpSender.Method.PUT
) )
open class App : Application(), LifecycleObserver { open class App : Application(), LifecycleObserver, ImageLoaderFactory {
private val preferences: PreferencesHelper by injectLazy() private val preferences: PreferencesHelper by injectLazy()
@ -67,6 +77,23 @@ open class App : Application(), LifecycleObserver {
LocaleHelper.updateConfiguration(this, newConfig, true) LocaleHelper.updateConfiguration(this, newConfig, true)
} }
override fun newImageLoader(): ImageLoader {
return ImageLoader.Builder(this).apply {
componentRegistry {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
add(ImageDecoderDecoder(this@App))
} else {
add(GifDecoder())
}
add(ByteBufferFetcher())
add(MangaCoverFetcher())
}
okHttpClient(Injekt.get<NetworkHelper>().coilClient)
crossfade(300)
allowRgb565(getSystemService<ActivityManager>()!!.isLowRamDevice)
}.build()
}
@OnLifecycleEvent(Lifecycle.Event.ON_STOP) @OnLifecycleEvent(Lifecycle.Event.ON_STOP)
@Suppress("unused") @Suppress("unused")
fun onAppBackgrounded() { fun onAppBackgrounded() {

View File

@ -1,6 +1,7 @@
package eu.kanade.tachiyomi.data.cache package eu.kanade.tachiyomi.data.cache
import android.content.Context import android.content.Context
import coil.imageLoader
import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.util.storage.DiskUtil import eu.kanade.tachiyomi.util.storage.DiskUtil
import java.io.File import java.io.File
@ -99,6 +100,13 @@ class CoverCache(private val context: Context) {
} }
} }
/**
* Clear coil's memory cache.
*/
fun clearMemoryCache() {
context.imageLoader.memoryCache.clear()
}
private fun getCacheDir(dir: String): File { private fun getCacheDir(dir: String): File {
return context.getExternalFilesDir(dir) return context.getExternalFilesDir(dir)
?: File(context.filesDir, dir).also { it.mkdirs() } ?: File(context.filesDir, dir).also { it.mkdirs() }

View File

@ -0,0 +1,25 @@
package eu.kanade.tachiyomi.data.coil
import coil.bitmap.BitmapPool
import coil.decode.DataSource
import coil.decode.Options
import coil.fetch.FetchResult
import coil.fetch.Fetcher
import coil.fetch.SourceResult
import coil.size.Size
import okio.buffer
import okio.source
import java.io.ByteArrayInputStream
import java.nio.ByteBuffer
class ByteBufferFetcher : Fetcher<ByteBuffer> {
override suspend fun fetch(pool: BitmapPool, data: ByteBuffer, size: Size, options: Options): FetchResult {
return SourceResult(
source = ByteArrayInputStream(data.array()).source().buffer(),
mimeType = null,
dataSource = DataSource.MEMORY
)
}
override fun key(data: ByteBuffer): String? = null
}

View File

@ -0,0 +1,172 @@
package eu.kanade.tachiyomi.data.coil
import coil.bitmap.BitmapPool
import coil.decode.DataSource
import coil.decode.Options
import coil.fetch.FetchResult
import coil.fetch.Fetcher
import coil.fetch.SourceResult
import coil.network.HttpException
import coil.request.get
import coil.size.Size
import eu.kanade.tachiyomi.data.cache.CoverCache
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.network.NetworkHelper
import eu.kanade.tachiyomi.network.await
import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.source.online.HttpSource
import okhttp3.CacheControl
import okhttp3.Call
import okhttp3.Request
import okhttp3.Response
import okhttp3.ResponseBody
import okio.buffer
import okio.sink
import okio.source
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
import java.io.File
import java.util.Date
/**
* Coil component that fetches [Manga] cover while using the cached file in disk when available.
*
* Available request parameter:
* - [USE_CUSTOM_COVER]: Use custom cover if set by user, default is true
*/
class MangaCoverFetcher : Fetcher<Manga> {
private val coverCache: CoverCache by injectLazy()
private val sourceManager: SourceManager by injectLazy()
private val defaultClient = Injekt.get<NetworkHelper>().coilClient
override fun key(data: Manga): String? {
if (data.thumbnail_url.isNullOrBlank()) return null
return data.thumbnail_url!!
}
override suspend fun fetch(pool: BitmapPool, data: Manga, size: Size, options: Options): FetchResult {
// Use custom cover if exists
val useCustomCover = options.parameters[USE_CUSTOM_COVER] as? Boolean ?: true
val customCoverFile = coverCache.getCustomCoverFile(data)
if (useCustomCover && customCoverFile.exists()) {
return fileLoader(customCoverFile)
}
val cover = data.thumbnail_url
return when (getResourceType(cover)) {
Type.URL -> httpLoader(data, options)
Type.File -> fileLoader(data)
null -> error("Invalid image")
}
}
private suspend fun httpLoader(manga: Manga, options: Options): FetchResult {
val coverFile = coverCache.getCoverFile(manga) ?: error("No cover specified")
// Use previously cached cover if exist
if (coverFile.exists() && options.diskCachePolicy.readEnabled) {
if (!manga.favorite) {
coverFile.setLastModified(Date().time)
}
return fileLoader(coverFile)
}
val (response, body) = awaitGetCall(manga, options)
if (!response.isSuccessful) {
body.close()
throw HttpException(response)
}
// Write to disk for future use
if (options.diskCachePolicy.writeEnabled) {
response.peekBody(Long.MAX_VALUE).source().use { input ->
val tmpFile = File(coverFile.absolutePath + "_tmp")
tmpFile.parentFile?.mkdirs()
tmpFile.sink().buffer().use { output ->
output.writeAll(input)
}
if (coverFile.exists()) {
coverFile.delete()
}
tmpFile.renameTo(coverFile)
}
}
return SourceResult(
source = body.source(),
mimeType = "image/*",
dataSource = if (response.cacheResponse != null) DataSource.DISK else DataSource.NETWORK
)
}
private suspend fun awaitGetCall(manga: Manga, options: Options): Pair<Response, ResponseBody> {
val call = getCall(manga, options)
val response = call.await()
return response to checkNotNull(response.body) { "Null response source" }
}
private fun getCall(manga: Manga, options: Options): Call {
val source = sourceManager.get(manga.source) as? HttpSource
val client = source?.client ?: defaultClient
val newClient = client.newBuilder().build()
val request = Request.Builder().url(manga.thumbnail_url!!).also {
if (source != null) {
it.headers(source.headers)
}
val networkRead = options.networkCachePolicy.readEnabled
val diskRead = options.diskCachePolicy.readEnabled
when {
!networkRead && diskRead -> {
it.cacheControl(CacheControl.FORCE_CACHE)
}
networkRead && !diskRead -> if (options.diskCachePolicy.writeEnabled) {
it.cacheControl(CacheControl.FORCE_NETWORK)
} else {
it.cacheControl(CACHE_CONTROL_FORCE_NETWORK_NO_CACHE)
}
!networkRead && !diskRead -> {
// This causes the request to fail with a 504 Unsatisfiable Request.
it.cacheControl(CACHE_CONTROL_NO_NETWORK_NO_CACHE)
}
}
}.build()
return newClient.newCall(request)
}
private fun fileLoader(manga: Manga): FetchResult {
return fileLoader(File(manga.thumbnail_url!!.substringAfter("file://")))
}
private fun fileLoader(file: File): FetchResult {
return SourceResult(
source = file.source().buffer(),
mimeType = "image/*",
dataSource = DataSource.DISK
)
}
private fun getResourceType(cover: String?): Type? {
return when {
cover.isNullOrEmpty() -> null
cover.startsWith("http") || cover.startsWith("Custom-", true) -> Type.URL
cover.startsWith("/") || cover.startsWith("file://") -> Type.File
else -> null
}
}
private enum class Type {
File, URL
}
companion object {
const val USE_CUSTOM_COVER = "use_custom_cover"
private val CACHE_CONTROL_FORCE_NETWORK_NO_CACHE = CacheControl.Builder().noCache().noStore().build()
private val CACHE_CONTROL_NO_NETWORK_NO_CACHE = CacheControl.Builder().noCache().onlyIfCached().build()
}
}

View File

@ -1,60 +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 timber.log.Timber
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 filePath: String = "") : DataFetcher<InputStream> {
private var data: InputStream? = null
override fun loadData(priority: Priority, callback: DataFetcher.DataCallback<in InputStream>) {
loadFromFile(callback)
}
private fun loadFromFile(callback: DataFetcher.DataCallback<in InputStream>) {
loadFromFile(File(filePath), callback)
}
protected fun loadFromFile(file: File, callback: DataFetcher.DataCallback<in InputStream>) {
try {
data = FileInputStream(file)
} catch (e: FileNotFoundException) {
if (Log.isLoggable(TAG, Log.DEBUG)) {
Timber.d(e, "Failed to open file")
}
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,25 +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.cache.CoverCache
import eu.kanade.tachiyomi.data.database.models.Manga
import java.io.File
import java.io.InputStream
import java.lang.Exception
open class LibraryMangaCustomCoverFetcher(
private val manga: Manga,
private val coverCache: CoverCache
) : FileFetcher() {
override fun loadData(priority: Priority, callback: DataFetcher.DataCallback<in InputStream>) {
getCustomCoverFile()?.let {
loadFromFile(it, callback)
} ?: callback.onLoadFailed(Exception("Custom cover file not found"))
}
protected fun getCustomCoverFile(): File? {
return coverCache.getCustomCoverFile(manga).takeIf { it.exists() }
}
}

View File

@ -1,86 +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.cache.CoverCache
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 coverCache: CoverCache
) : LibraryMangaCustomCoverFetcher(manga, coverCache) {
override fun loadData(priority: Priority, callback: DataFetcher.DataCallback<in InputStream>) {
getCustomCoverFile()?.let {
loadFromFile(it, callback)
return
}
val cover = coverCache.getCoverFile(manga)
if (cover == null) {
callback.onLoadFailed(Exception("Null thumbnail url"))
return
}
if (!cover.exists()) {
networkFetcher.loadData(
priority,
object : DataFetcher.DataCallback<InputStream> {
override fun onDataReady(data: InputStream?) {
if (data != null) {
val tmpFile = File(cover.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(cover)
loadFromFile(cover, 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(cover, callback)
}
}
override fun cleanup() {
super.cleanup()
networkFetcher.cleanup()
}
override fun cancel() {
super.cancel()
networkFetcher.cancel()
}
}

View File

@ -1,15 +0,0 @@
package eu.kanade.tachiyomi.data.glide
import com.bumptech.glide.load.Key
import eu.kanade.tachiyomi.data.database.models.Manga
import java.security.MessageDigest
data class MangaThumbnail(val manga: Manga, val coverLastModified: Long) : Key {
val key = manga.url + coverLastModified
override fun updateDiskCacheKey(messageDigest: MessageDigest) {
messageDigest.update(key.toByteArray(Key.CHARSET))
}
}
fun Manga.toMangaThumbnail() = MangaThumbnail(this, cover_last_modified)

View File

@ -1,134 +0,0 @@
package eu.kanade.tachiyomi.data.glide
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 eu.kanade.tachiyomi.util.isLocal
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
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 MangaThumbnailModelLoader : ModelLoader<MangaThumbnail, 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
/**
* Map where request headers are stored for a source.
*/
private val cachedHeaders = hashMapOf<Long, LazyHeaders>()
/**
* Factory class for creating [MangaThumbnailModelLoader] instances.
*/
class Factory : ModelLoaderFactory<MangaThumbnail, InputStream> {
override fun build(multiFactory: MultiModelLoaderFactory): ModelLoader<MangaThumbnail, InputStream> {
return MangaThumbnailModelLoader()
}
override fun teardown() {}
}
override fun handles(model: MangaThumbnail): Boolean {
return true
}
/**
* Returns a fetcher for the given manga or null if the url is empty.
*
* @param mangaThumbnail 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(
mangaThumbnail: MangaThumbnail,
width: Int,
height: Int,
options: Options
): ModelLoader.LoadData<InputStream>? {
val manga = mangaThumbnail.manga
val url = manga.thumbnail_url
if (url.isNullOrEmpty()) {
return if (!manga.favorite || manga.isLocal()) {
null
} else {
ModelLoader.LoadData(mangaThumbnail, LibraryMangaCustomCoverFetcher(manga, coverCache))
}
}
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)
}
val libraryFetcher = LibraryMangaUrlFetcher(networkFetcher, manga, coverCache)
// Return an instance of the fetcher providing the needed elements.
return ModelLoader.LoadData(mangaThumbnail, libraryFetcher)
} else {
// Return an instance of the fetcher providing the needed elements.
return ModelLoader.LoadData(mangaThumbnail, FileFetcher(url.removePrefix("file://")))
}
}
/**
* Returns the request headers for a source copying its OkHttp headers and caching them.
*
* @param manga the model.
*/
private 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()
}
}
}

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,55 +0,0 @@
package eu.kanade.tachiyomi.data.glide
import android.content.Context
import android.graphics.drawable.Drawable
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.load.resource.drawable.DrawableTransitionOptions
import com.bumptech.glide.module.AppGlideModule
import com.bumptech.glide.request.RequestOptions
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))
builder.setDefaultTransitionOptions(
Drawable::class.java,
DrawableTransitionOptions.withCrossFade()
)
}
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(
MangaThumbnail::class.java,
InputStream::class.java,
MangaThumbnailModelLoader.Factory()
)
registry.append(
InputStream::class.java,
InputStream::class.java,
PassthroughModelLoader.Factory()
)
}
}

View File

@ -6,19 +6,22 @@ import android.content.Context
import android.content.Intent import android.content.Intent
import android.graphics.Bitmap import android.graphics.Bitmap
import android.graphics.BitmapFactory import android.graphics.BitmapFactory
import android.graphics.drawable.BitmapDrawable
import android.net.Uri import android.net.Uri
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat import androidx.core.app.NotificationManagerCompat
import com.bumptech.glide.Glide import coil.imageLoader
import coil.request.ImageRequest
import coil.transform.CircleCropTransformation
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.Chapter import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.glide.toMangaThumbnail
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
import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.ui.main.MainActivity import eu.kanade.tachiyomi.ui.main.MainActivity
import eu.kanade.tachiyomi.util.lang.chop import eu.kanade.tachiyomi.util.lang.chop
import eu.kanade.tachiyomi.util.lang.launchUI
import eu.kanade.tachiyomi.util.system.notification import eu.kanade.tachiyomi.util.system.notification
import eu.kanade.tachiyomi.util.system.notificationBuilder import eu.kanade.tachiyomi.util.system.notificationBuilder
import eu.kanade.tachiyomi.util.system.notificationManager import eu.kanade.tachiyomi.util.system.notificationManager
@ -165,14 +168,17 @@ class LibraryUpdateNotifier(private val context: Context) {
// Per-manga notification // Per-manga notification
if (!preferences.hideNotificationContent()) { if (!preferences.hideNotificationContent()) {
updates.forEach { (manga, chapters) -> launchUI {
notify(manga.id.hashCode(), createNewChaptersNotification(manga, chapters)) updates.forEach { (manga, chapters) ->
notify(manga.id.hashCode(), createNewChaptersNotification(manga, chapters))
}
} }
} }
} }
} }
private fun createNewChaptersNotification(manga: Manga, chapters: Array<Chapter>): Notification { private suspend fun createNewChaptersNotification(manga: Manga, chapters: Array<Chapter>): Notification {
val icon = getMangaIcon(manga)
return context.notification(Notifications.CHANNEL_NEW_CHAPTERS) { return context.notification(Notifications.CHANNEL_NEW_CHAPTERS) {
setContentTitle(manga.title) setContentTitle(manga.title)
@ -182,7 +188,6 @@ class LibraryUpdateNotifier(private val context: Context) {
setSmallIcon(R.drawable.ic_tachi) setSmallIcon(R.drawable.ic_tachi)
val icon = getMangaIcon(manga)
if (icon != null) { if (icon != null) {
setLargeIcon(icon) setLargeIcon(icon)
} }
@ -226,23 +231,14 @@ class LibraryUpdateNotifier(private val context: Context) {
context.notificationManager.cancel(Notifications.ID_LIBRARY_PROGRESS) context.notificationManager.cancel(Notifications.ID_LIBRARY_PROGRESS)
} }
private fun getMangaIcon(manga: Manga): Bitmap? { private suspend fun getMangaIcon(manga: Manga): Bitmap? {
return try { val request = ImageRequest.Builder(context)
Glide.with(context) .data(manga)
.asBitmap() .transformations(CircleCropTransformation())
.load(manga.toMangaThumbnail()) .size(NOTIF_ICON_SIZE)
.dontTransform() .build()
.centerCrop() val drawable = context.imageLoader.execute(request).drawable
.circleCrop() return (drawable as? BitmapDrawable)?.bitmap
.override(
NOTIF_ICON_SIZE,
NOTIF_ICON_SIZE
)
.submit()
.get()
} catch (e: Exception) {
null
}
} }
private fun getNewChaptersDescription(chapters: Array<Chapter>): String { private fun getNewChaptersDescription(chapters: Array<Chapter>): String {

View File

@ -386,6 +386,7 @@ class LibraryUpdateService(
} }
} }
coverCache.clearMemoryCache()
notifier.cancelProgressNotification() notifier.cancelProgressNotification()
} }

View File

@ -1,6 +1,7 @@
package eu.kanade.tachiyomi.network package eu.kanade.tachiyomi.network
import android.content.Context import android.content.Context
import coil.util.CoilUtils
import eu.kanade.tachiyomi.BuildConfig import eu.kanade.tachiyomi.BuildConfig
import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import okhttp3.Cache import okhttp3.Cache
@ -20,28 +21,32 @@ class NetworkHelper(context: Context) {
val cookieManager = AndroidCookieJar() val cookieManager = AndroidCookieJar()
val client by lazy { private val baseClientBuilder: OkHttpClient.Builder
val builder = OkHttpClient.Builder() get() {
.cookieJar(cookieManager) val builder = OkHttpClient.Builder()
.cache(Cache(cacheDir, cacheSize)) .cookieJar(cookieManager)
.connectTimeout(30, TimeUnit.SECONDS) .connectTimeout(30, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS) .readTimeout(30, TimeUnit.SECONDS)
.addInterceptor(UserAgentInterceptor()) .addInterceptor(UserAgentInterceptor())
if (BuildConfig.DEBUG) { if (BuildConfig.DEBUG) {
val httpLoggingInterceptor = HttpLoggingInterceptor().apply { val httpLoggingInterceptor = HttpLoggingInterceptor().apply {
level = HttpLoggingInterceptor.Level.HEADERS level = HttpLoggingInterceptor.Level.HEADERS
}
builder.addInterceptor(httpLoggingInterceptor)
} }
builder.addInterceptor(httpLoggingInterceptor)
when (preferences.dohProvider()) {
PREF_DOH_CLOUDFLARE -> builder.dohCloudflare()
PREF_DOH_GOOGLE -> builder.dohGoogle()
}
return builder
} }
when (preferences.dohProvider()) { val client by lazy { baseClientBuilder.cache(Cache(cacheDir, cacheSize)).build() }
PREF_DOH_CLOUDFLARE -> builder.dohCloudflare()
PREF_DOH_GOOGLE -> builder.dohGoogle()
}
builder.build() val coilClient by lazy { baseClientBuilder.cache(CoilUtils.createDefaultCache(context)).build() }
}
val cloudflareClient by lazy { val cloudflareClient by lazy {
client.newBuilder() client.newBuilder()

View File

@ -1,9 +1,10 @@
package eu.kanade.tachiyomi.ui.browse.extension package eu.kanade.tachiyomi.ui.browse.extension
import android.view.View import android.view.View
import coil.clear
import coil.load
import eu.davidea.viewholders.FlexibleViewHolder import eu.davidea.viewholders.FlexibleViewHolder
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.databinding.ExtensionCardItemBinding import eu.kanade.tachiyomi.databinding.ExtensionCardItemBinding
import eu.kanade.tachiyomi.extension.model.Extension import eu.kanade.tachiyomi.extension.model.Extension
@ -41,11 +42,9 @@ class ExtensionHolder(view: View, val adapter: ExtensionAdapter) :
else -> "" else -> ""
}.toUpperCase() }.toUpperCase()
GlideApp.with(itemView.context).clear(binding.image) binding.image.clear()
if (extension is Extension.Available) { if (extension is Extension.Available) {
GlideApp.with(itemView.context) binding.image.load(extension.iconUrl)
.load(extension.iconUrl)
.into(binding.image)
} else { } else {
extension.getApplicationIcon(itemView.context)?.let { binding.image.setImageDrawable(it) } extension.getApplicationIcon(itemView.context)?.let { binding.image.setImageDrawable(it) }
} }

View File

@ -1,14 +1,11 @@
package eu.kanade.tachiyomi.ui.browse.migration.manga package eu.kanade.tachiyomi.ui.browse.migration.manga
import android.view.View import android.view.View
import com.bumptech.glide.load.engine.DiskCacheStrategy import coil.clear
import com.bumptech.glide.load.resource.bitmap.CenterCrop import coil.loadAny
import com.bumptech.glide.load.resource.bitmap.RoundedCorners import coil.transform.RoundedCornersTransformation
import com.bumptech.glide.request.RequestOptions
import eu.davidea.viewholders.FlexibleViewHolder import eu.davidea.viewholders.FlexibleViewHolder
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.glide.GlideApp
import eu.kanade.tachiyomi.data.glide.toMangaThumbnail
import eu.kanade.tachiyomi.databinding.SourceListItemBinding import eu.kanade.tachiyomi.databinding.SourceListItemBinding
class MigrationMangaHolder( class MigrationMangaHolder(
@ -28,15 +25,10 @@ class MigrationMangaHolder(
binding.title.text = item.manga.title binding.title.text = item.manga.title
// Update the cover. // Update the cover.
GlideApp.with(itemView.context).clear(binding.thumbnail) val radius = itemView.context.resources.getDimension(R.dimen.card_radius)
binding.thumbnail.clear()
val radius = itemView.context.resources.getDimensionPixelSize(R.dimen.card_radius) binding.thumbnail.loadAny(item.manga) {
val requestOptions = RequestOptions().transform(CenterCrop(), RoundedCorners(radius)) transformations(RoundedCornersTransformation(radius))
GlideApp.with(itemView.context) }
.load(item.manga.toMangaThumbnail())
.diskCacheStrategy(DiskCacheStrategy.RESOURCE)
.apply(requestOptions)
.dontAnimate()
.into(binding.thumbnail)
} }
} }

View File

@ -1,11 +1,14 @@
package eu.kanade.tachiyomi.ui.browse.source.browse package eu.kanade.tachiyomi.ui.browse.source.browse
import android.view.View import android.view.View
import com.bumptech.glide.load.engine.DiskCacheStrategy import coil.clear
import coil.imageLoader
import coil.request.CachePolicy
import coil.request.ImageRequest
import coil.transition.CrossfadeTransition
import eu.davidea.flexibleadapter.FlexibleAdapter import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.kanade.tachiyomi.data.coil.MangaCoverFetcher
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.data.glide.toMangaThumbnail
import eu.kanade.tachiyomi.databinding.SourceComfortableGridItemBinding import eu.kanade.tachiyomi.databinding.SourceComfortableGridItemBinding
import eu.kanade.tachiyomi.widget.StateImageViewTarget import eu.kanade.tachiyomi.widget.StateImageViewTarget
@ -42,14 +45,18 @@ class SourceComfortableGridHolder(private val view: View, private val adapter: F
// For rounded corners // For rounded corners
binding.card.clipToOutline = true binding.card.clipToOutline = true
GlideApp.with(view.context).clear(binding.thumbnail) binding.thumbnail.clear()
if (!manga.thumbnail_url.isNullOrEmpty()) { if (!manga.thumbnail_url.isNullOrEmpty()) {
GlideApp.with(view.context) val crossfadeDuration = view.context.imageLoader.defaults.transition.let {
.load(manga.toMangaThumbnail()) if (it is CrossfadeTransition) it.durationMillis else 0
.diskCacheStrategy(DiskCacheStrategy.DATA) }
.centerCrop() val request = ImageRequest.Builder(view.context)
.placeholder(android.R.color.transparent) .data(manga)
.into(StateImageViewTarget(binding.thumbnail, binding.progress)) .setParameter(MangaCoverFetcher.USE_CUSTOM_COVER, false)
.diskCachePolicy(CachePolicy.DISABLED)
.target(StateImageViewTarget(binding.thumbnail, binding.progress, crossfadeDuration))
.build()
itemView.context.imageLoader.enqueue(request)
} }
} }
} }

View File

@ -1,11 +1,14 @@
package eu.kanade.tachiyomi.ui.browse.source.browse package eu.kanade.tachiyomi.ui.browse.source.browse
import android.view.View import android.view.View
import com.bumptech.glide.load.engine.DiskCacheStrategy import coil.clear
import coil.imageLoader
import coil.request.CachePolicy
import coil.request.ImageRequest
import coil.transition.CrossfadeTransition
import eu.davidea.flexibleadapter.FlexibleAdapter import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.kanade.tachiyomi.data.coil.MangaCoverFetcher
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.data.glide.toMangaThumbnail
import eu.kanade.tachiyomi.databinding.SourceComfortableGridItemBinding import eu.kanade.tachiyomi.databinding.SourceComfortableGridItemBinding
import eu.kanade.tachiyomi.widget.StateImageViewTarget import eu.kanade.tachiyomi.widget.StateImageViewTarget
@ -42,14 +45,18 @@ open class SourceGridHolder(private val view: View, private val adapter: Flexibl
// For rounded corners // For rounded corners
binding.card.clipToOutline = true binding.card.clipToOutline = true
GlideApp.with(view.context).clear(binding.thumbnail) binding.thumbnail.clear()
if (!manga.thumbnail_url.isNullOrEmpty()) { if (!manga.thumbnail_url.isNullOrEmpty()) {
GlideApp.with(view.context) val crossfadeDuration = view.context.imageLoader.defaults.transition.let {
.load(manga.toMangaThumbnail()) if (it is CrossfadeTransition) it.durationMillis else 0
.diskCacheStrategy(DiskCacheStrategy.DATA) }
.centerCrop() val request = ImageRequest.Builder(view.context)
.placeholder(android.R.color.transparent) .data(manga)
.into(StateImageViewTarget(binding.thumbnail, binding.progress)) .setParameter(MangaCoverFetcher.USE_CUSTOM_COVER, false)
.diskCachePolicy(CachePolicy.DISABLED)
.target(StateImageViewTarget(binding.thumbnail, binding.progress, crossfadeDuration))
.build()
itemView.context.imageLoader.enqueue(request)
} }
} }
} }

View File

@ -1,15 +1,14 @@
package eu.kanade.tachiyomi.ui.browse.source.browse package eu.kanade.tachiyomi.ui.browse.source.browse
import android.view.View import android.view.View
import com.bumptech.glide.load.engine.DiskCacheStrategy import coil.clear
import com.bumptech.glide.load.resource.bitmap.CenterCrop import coil.loadAny
import com.bumptech.glide.load.resource.bitmap.RoundedCorners import coil.request.CachePolicy
import com.bumptech.glide.request.RequestOptions import coil.transform.RoundedCornersTransformation
import eu.davidea.flexibleadapter.FlexibleAdapter import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.coil.MangaCoverFetcher
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.data.glide.toMangaThumbnail
import eu.kanade.tachiyomi.databinding.SourceListItemBinding import eu.kanade.tachiyomi.databinding.SourceListItemBinding
import eu.kanade.tachiyomi.util.system.getResourceColor import eu.kanade.tachiyomi.util.system.getResourceColor
@ -46,18 +45,14 @@ class SourceListHolder(private val view: View, adapter: FlexibleAdapter<*>) :
} }
override fun setImage(manga: Manga) { override fun setImage(manga: Manga) {
GlideApp.with(view.context).clear(binding.thumbnail) binding.thumbnail.clear()
if (!manga.thumbnail_url.isNullOrEmpty()) { if (!manga.thumbnail_url.isNullOrEmpty()) {
val radius = view.context.resources.getDimensionPixelSize(R.dimen.card_radius) val radius = view.context.resources.getDimension(R.dimen.card_radius)
val requestOptions = RequestOptions().transform(CenterCrop(), RoundedCorners(radius)) binding.thumbnail.loadAny(manga) {
GlideApp.with(view.context) setParameter(MangaCoverFetcher.USE_CUSTOM_COVER, false)
.load(manga.toMangaThumbnail()) transformations(RoundedCornersTransformation(radius))
.diskCacheStrategy(DiskCacheStrategy.DATA) diskCachePolicy(CachePolicy.DISABLED)
.apply(requestOptions) }
.dontAnimate()
.placeholder(android.R.color.transparent)
.into(binding.thumbnail)
} }
} }
} }

View File

@ -1,11 +1,14 @@
package eu.kanade.tachiyomi.ui.browse.source.globalsearch package eu.kanade.tachiyomi.ui.browse.source.globalsearch
import android.view.View import android.view.View
import com.bumptech.glide.load.engine.DiskCacheStrategy import coil.clear
import coil.imageLoader
import coil.request.CachePolicy
import coil.request.ImageRequest
import coil.transition.CrossfadeTransition
import eu.davidea.viewholders.FlexibleViewHolder import eu.davidea.viewholders.FlexibleViewHolder
import eu.kanade.tachiyomi.data.coil.MangaCoverFetcher
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.data.glide.toMangaThumbnail
import eu.kanade.tachiyomi.databinding.GlobalSearchControllerCardItemBinding import eu.kanade.tachiyomi.databinding.GlobalSearchControllerCardItemBinding
import eu.kanade.tachiyomi.widget.StateImageViewTarget import eu.kanade.tachiyomi.widget.StateImageViewTarget
@ -42,15 +45,18 @@ class GlobalSearchCardHolder(view: View, adapter: GlobalSearchCardAdapter) :
} }
fun setImage(manga: Manga) { fun setImage(manga: Manga) {
GlideApp.with(itemView.context).clear(binding.cover) binding.cover.clear()
if (!manga.thumbnail_url.isNullOrEmpty()) { if (!manga.thumbnail_url.isNullOrEmpty()) {
GlideApp.with(itemView.context) val crossfadeDuration = itemView.context.imageLoader.defaults.transition.let {
.load(manga.toMangaThumbnail()) if (it is CrossfadeTransition) it.durationMillis else 0
.diskCacheStrategy(DiskCacheStrategy.DATA) }
.centerCrop() val request = ImageRequest.Builder(itemView.context)
.skipMemoryCache(true) .data(manga)
.placeholder(android.R.color.transparent) .setParameter(MangaCoverFetcher.USE_CUSTOM_COVER, false)
.into(StateImageViewTarget(binding.cover, binding.progress)) .diskCachePolicy(CachePolicy.DISABLED)
.target(StateImageViewTarget(binding.cover, binding.progress, crossfadeDuration))
.build()
itemView.context.imageLoader.enqueue(request)
} }
} }
} }

View File

@ -3,11 +3,10 @@ package eu.kanade.tachiyomi.ui.library
import android.view.View import android.view.View
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.load.engine.DiskCacheStrategy import coil.clear
import coil.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.data.glide.toMangaThumbnail
import eu.kanade.tachiyomi.databinding.SourceComfortableGridItemBinding import eu.kanade.tachiyomi.databinding.SourceComfortableGridItemBinding
import eu.kanade.tachiyomi.util.isLocal import eu.kanade.tachiyomi.util.isLocal
@ -57,12 +56,7 @@ class LibraryComfortableGridHolder(
binding.card.clipToOutline = true binding.card.clipToOutline = true
// Update the cover. // Update the cover.
GlideApp.with(view.context).clear(binding.thumbnail) binding.thumbnail.clear()
GlideApp.with(view.context) binding.thumbnail.loadAny(item.manga)
.load(item.manga.toMangaThumbnail())
.diskCacheStrategy(DiskCacheStrategy.RESOURCE)
.centerCrop()
.dontAnimate()
.into(binding.thumbnail)
} }
} }

View File

@ -2,10 +2,9 @@ package eu.kanade.tachiyomi.ui.library
import android.view.View import android.view.View
import androidx.core.view.isVisible import androidx.core.view.isVisible
import com.bumptech.glide.load.engine.DiskCacheStrategy import coil.clear
import coil.loadAny
import eu.davidea.flexibleadapter.FlexibleAdapter import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.kanade.tachiyomi.data.glide.GlideApp
import eu.kanade.tachiyomi.data.glide.toMangaThumbnail
import eu.kanade.tachiyomi.databinding.SourceCompactGridItemBinding import eu.kanade.tachiyomi.databinding.SourceCompactGridItemBinding
import eu.kanade.tachiyomi.util.isLocal import eu.kanade.tachiyomi.util.isLocal
@ -55,12 +54,7 @@ open class LibraryCompactGridHolder(
binding.card.clipToOutline = true binding.card.clipToOutline = true
// Update the cover. // Update the cover.
GlideApp.with(view.context).clear(binding.thumbnail) binding.thumbnail.clear()
GlideApp.with(view.context) binding.thumbnail.loadAny(item.manga)
.load(item.manga.toMangaThumbnail())
.diskCacheStrategy(DiskCacheStrategy.RESOURCE)
.centerCrop()
.dontAnimate()
.into(binding.thumbnail)
} }
} }

View File

@ -2,14 +2,11 @@ package eu.kanade.tachiyomi.ui.library
import android.view.View import android.view.View
import androidx.core.view.isVisible import androidx.core.view.isVisible
import com.bumptech.glide.load.engine.DiskCacheStrategy import coil.clear
import com.bumptech.glide.load.resource.bitmap.CenterCrop import coil.loadAny
import com.bumptech.glide.load.resource.bitmap.RoundedCorners import coil.transform.RoundedCornersTransformation
import com.bumptech.glide.request.RequestOptions
import eu.davidea.flexibleadapter.FlexibleAdapter import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.glide.GlideApp
import eu.kanade.tachiyomi.data.glide.toMangaThumbnail
import eu.kanade.tachiyomi.databinding.SourceListItemBinding import eu.kanade.tachiyomi.databinding.SourceListItemBinding
import eu.kanade.tachiyomi.util.isLocal import eu.kanade.tachiyomi.util.isLocal
@ -62,15 +59,10 @@ class LibraryListHolder(
} }
// Update the cover. // Update the cover.
GlideApp.with(itemView.context).clear(binding.thumbnail) val radius = view.context.resources.getDimension(R.dimen.card_radius)
binding.thumbnail.clear()
val radius = view.context.resources.getDimensionPixelSize(R.dimen.card_radius) binding.thumbnail.loadAny(item.manga) {
val requestOptions = RequestOptions().transform(CenterCrop(), RoundedCorners(radius)) transformations(RoundedCornersTransformation(radius))
GlideApp.with(itemView.context) }
.load(item.manga.toMangaThumbnail())
.diskCacheStrategy(DiskCacheStrategy.RESOURCE)
.apply(requestOptions)
.dontAnimate()
.into(binding.thumbnail)
} }
} }

View File

@ -284,6 +284,7 @@ class MangaPresenter(
} else if (manga.favorite) { } else if (manga.favorite) {
coverCache.setCustomCoverToCache(manga, it) coverCache.setCustomCoverToCache(manga, it)
manga.updateCoverLastModified(db) manga.updateCoverLastModified(db)
coverCache.clearMemoryCache()
} }
} }
} }
@ -300,6 +301,7 @@ class MangaPresenter(
.fromCallable { .fromCallable {
coverCache.deleteCustomCover(manga) coverCache.deleteCustomCover(manga)
manga.updateCoverLastModified(db) manga.updateCoverLastModified(db)
coverCache.clearMemoryCache()
} }
.subscribeOn(Schedulers.io()) .subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())

View File

@ -5,12 +5,9 @@ import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.load.engine.DiskCacheStrategy import coil.loadAny
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.data.glide.MangaThumbnail
import eu.kanade.tachiyomi.data.glide.toMangaThumbnail
import eu.kanade.tachiyomi.data.track.TrackManager import eu.kanade.tachiyomi.data.track.TrackManager
import eu.kanade.tachiyomi.databinding.MangaInfoHeaderBinding import eu.kanade.tachiyomi.databinding.MangaInfoHeaderBinding
import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.source.Source
@ -44,7 +41,6 @@ class MangaInfoHeaderAdapter(
private lateinit var binding: MangaInfoHeaderBinding private lateinit var binding: MangaInfoHeaderBinding
private var initialLoad: Boolean = true private var initialLoad: Boolean = true
private var currentMangaThumbnail: MangaThumbnail? = null
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): HeaderViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): HeaderViewHolder {
binding = MangaInfoHeaderBinding.inflate(LayoutInflater.from(parent.context), parent, false) binding = MangaInfoHeaderBinding.inflate(LayoutInflater.from(parent.context), parent, false)
@ -246,17 +242,8 @@ class MangaInfoHeaderAdapter(
setFavoriteButtonState(manga.favorite) setFavoriteButtonState(manga.favorite)
// Set cover if changed. // Set cover if changed.
val mangaThumbnail = manga.toMangaThumbnail() listOf(binding.mangaCover, binding.backdrop).forEach {
if (mangaThumbnail != currentMangaThumbnail) { it.loadAny(manga)
currentMangaThumbnail = mangaThumbnail
listOf(binding.mangaCover, binding.backdrop)
.forEach {
GlideApp.with(view.context)
.load(mangaThumbnail)
.diskCacheStrategy(DiskCacheStrategy.RESOURCE)
.centerCrop()
.into(it)
}
} }
// Manga info section // Manga info section

View File

@ -5,9 +5,9 @@ import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.ArrayAdapter import android.widget.ArrayAdapter
import androidx.core.view.isVisible import androidx.core.view.isVisible
import com.bumptech.glide.load.engine.DiskCacheStrategy import coil.clear
import coil.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.databinding.TrackSearchItemBinding import eu.kanade.tachiyomi.databinding.TrackSearchItemBinding
import eu.kanade.tachiyomi.util.view.inflate import eu.kanade.tachiyomi.util.view.inflate
@ -46,13 +46,9 @@ class TrackSearchAdapter(context: Context) :
fun onSetValues(track: TrackSearch) { fun onSetValues(track: TrackSearch) {
binding.trackSearchTitle.text = track.title binding.trackSearchTitle.text = track.title
binding.trackSearchSummary.text = track.summary binding.trackSearchSummary.text = track.summary
GlideApp.with(view.context).clear(binding.trackSearchCover) binding.trackSearchCover.clear()
if (track.cover_url.isNotEmpty()) { if (track.cover_url.isNotEmpty()) {
GlideApp.with(view.context) binding.trackSearchCover.load(track.cover_url)
.load(track.cover_url)
.diskCacheStrategy(DiskCacheStrategy.RESOURCE)
.centerCrop()
.into(binding.trackSearchCover)
} }
val hasStatus = track.publishing_status.isNotBlank() val hasStatus = track.publishing_status.isNotBlank()

View File

@ -624,6 +624,7 @@ class ReaderPresenter(
if (manga.favorite) { if (manga.favorite) {
coverCache.setCustomCoverToCache(manga, stream()) coverCache.setCustomCoverToCache(manga, stream())
manga.updateCoverLastModified(db) manga.updateCoverLastModified(db)
coverCache.clearMemoryCache()
SetAsCoverResult.Success SetAsCoverResult.Success
} else { } else {
SetAsCoverResult.AddToLibraryFirst SetAsCoverResult.AddToLibraryFirst

View File

@ -2,10 +2,12 @@ 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 com.bumptech.glide.load.engine.DiskCacheStrategy import coil.imageLoader
import coil.request.CachePolicy
import coil.request.ImageRequest
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
@ -30,25 +32,25 @@ class SaveImageNotifier(private val context: Context) {
get() = Notifications.ID_DOWNLOAD_IMAGE get() = Notifications.ID_DOWNLOAD_IMAGE
/** /**
* Called when image download/copy is complete. This method must be called in a background * Called when image download/copy is complete.
* thread.
* *
* @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) val request = ImageRequest.Builder(context)
.asBitmap() .data(file)
.load(file) .memoryCachePolicy(CachePolicy.DISABLED)
.diskCacheStrategy(DiskCacheStrategy.NONE) .size(720, 1280)
.skipMemoryCache(true) .target(
.submit(720, 1280) onSuccess = { result ->
.get() showCompleteNotification(file, (result as BitmapDrawable).bitmap)
},
if (bitmap != null) { onError = {
showCompleteNotification(file, bitmap) onError(null)
} else { }
onError(null) )
} .build()
context.imageLoader.enqueue(request)
} }
private fun showCompleteNotification(file: File, image: Bitmap) { private fun showCompleteNotification(file: File, image: Bitmap) {

View File

@ -2,7 +2,7 @@ package eu.kanade.tachiyomi.ui.reader.viewer.pager
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.graphics.PointF import android.graphics.PointF
import android.graphics.drawable.Drawable import android.graphics.drawable.Animatable
import android.view.GestureDetector import android.view.GestureDetector
import android.view.Gravity import android.view.Gravity
import android.view.MotionEvent import android.view.MotionEvent
@ -14,19 +14,13 @@ import android.widget.ImageView
import android.widget.LinearLayout import android.widget.LinearLayout
import android.widget.TextView import android.widget.TextView
import androidx.core.view.isVisible import androidx.core.view.isVisible
import com.bumptech.glide.load.DataSource import coil.imageLoader
import com.bumptech.glide.load.engine.DiskCacheStrategy import coil.request.CachePolicy
import com.bumptech.glide.load.engine.GlideException import coil.request.ImageRequest
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.source.model.Page import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.ui.reader.model.InsertPage import eu.kanade.tachiyomi.ui.reader.model.InsertPage
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
@ -41,6 +35,7 @@ import rx.Subscription
import rx.android.schedulers.AndroidSchedulers import rx.android.schedulers.AndroidSchedulers
import rx.schedulers.Schedulers import rx.schedulers.Schedulers
import java.io.InputStream import java.io.InputStream
import java.nio.ByteBuffer
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
/** /**
@ -480,38 +475,24 @@ 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) val request = ImageRequest.Builder(context)
.load(stream) .data(ByteBuffer.wrap(stream.readBytes()))
.skipMemoryCache(true) .memoryCachePolicy(CachePolicy.DISABLED)
.diskCacheStrategy(DiskCacheStrategy.NONE) .diskCachePolicy(CachePolicy.DISABLED)
.transition(DrawableTransitionOptions.with(NoTransition.getFactory())) .target(
.listener( onSuccess = { result ->
object : RequestListener<Drawable> { if (result is Animatable) {
override fun onLoadFailed( result.start()
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
} }
setImageDrawable(result)
onImageDecoded()
},
onError = {
onImageDecodeError()
} }
) )
.into(this) .crossfade(false)
.build()
context.imageLoader.enqueue(request)
} }
} }

View File

@ -2,7 +2,7 @@ package eu.kanade.tachiyomi.ui.reader.viewer.webtoon
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.res.Resources import android.content.res.Resources
import android.graphics.drawable.Drawable import android.graphics.drawable.Animatable
import android.view.Gravity import android.view.Gravity
import android.view.ViewGroup import android.view.ViewGroup
import android.view.ViewGroup.LayoutParams.MATCH_PARENT import android.view.ViewGroup.LayoutParams.MATCH_PARENT
@ -14,18 +14,13 @@ 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 androidx.core.view.isVisible import androidx.core.view.isVisible
import com.bumptech.glide.load.DataSource import coil.clear
import com.bumptech.glide.load.engine.DiskCacheStrategy import coil.imageLoader
import com.bumptech.glide.load.engine.GlideException import coil.request.CachePolicy
import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions import coil.request.ImageRequest
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
@ -37,6 +32,7 @@ import rx.Subscription
import rx.android.schedulers.AndroidSchedulers import rx.android.schedulers.AndroidSchedulers
import rx.schedulers.Schedulers import rx.schedulers.Schedulers
import java.io.InputStream import java.io.InputStream
import java.nio.ByteBuffer
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
/** /**
@ -146,7 +142,7 @@ class WebtoonPageHolder(
removeDecodeErrorLayout() removeDecodeErrorLayout()
subsamplingImageView?.recycle() subsamplingImageView?.recycle()
subsamplingImageView?.isVisible = false subsamplingImageView?.isVisible = false
imageView?.let { GlideApp.with(frame).clear(it) } imageView?.clear()
imageView?.isVisible = false imageView?.isVisible = false
progressBar.setProgress(0) progressBar.setProgress(0)
} }
@ -512,38 +508,24 @@ 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) val request = ImageRequest.Builder(context)
.load(stream) .data(ByteBuffer.wrap(stream.readBytes()))
.skipMemoryCache(true) .memoryCachePolicy(CachePolicy.DISABLED)
.diskCacheStrategy(DiskCacheStrategy.NONE) .diskCachePolicy(CachePolicy.DISABLED)
.transition(DrawableTransitionOptions.with(NoTransition.getFactory())) .target(
.listener( onSuccess = { result ->
object : RequestListener<Drawable> { if (result is Animatable) {
override fun onLoadFailed( result.start()
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
} }
setImageDrawable(result)
onImageDecoded()
},
onError = {
onImageDecodeError()
} }
) )
.into(this) .crossfade(false)
.build()
context.imageLoader.enqueue(request)
} }
} }

View File

@ -1,15 +1,12 @@
package eu.kanade.tachiyomi.ui.recent.history package eu.kanade.tachiyomi.ui.recent.history
import android.view.View import android.view.View
import com.bumptech.glide.load.engine.DiskCacheStrategy import coil.clear
import com.bumptech.glide.load.resource.bitmap.CenterCrop import coil.loadAny
import com.bumptech.glide.load.resource.bitmap.RoundedCorners import coil.transform.RoundedCornersTransformation
import com.bumptech.glide.request.RequestOptions
import eu.davidea.viewholders.FlexibleViewHolder import eu.davidea.viewholders.FlexibleViewHolder
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.data.glide.toMangaThumbnail
import eu.kanade.tachiyomi.databinding.HistoryItemBinding import eu.kanade.tachiyomi.databinding.HistoryItemBinding
import eu.kanade.tachiyomi.util.lang.toTimestampString import eu.kanade.tachiyomi.util.lang.toTimestampString
import java.util.Date import java.util.Date
@ -68,15 +65,11 @@ class HistoryHolder(
binding.mangaSubtitle.text = Date(history.last_read).toTimestampString() binding.mangaSubtitle.text = Date(history.last_read).toTimestampString()
} }
val radius = itemView.context.resources.getDimensionPixelSize(R.dimen.card_radius)
val requestOptions = RequestOptions().transform(CenterCrop(), RoundedCorners(radius))
// Set cover // Set cover
GlideApp.with(itemView.context).clear(binding.cover) val radius = itemView.context.resources.getDimension(R.dimen.card_radius)
GlideApp.with(itemView.context) binding.cover.clear()
.load(manga.toMangaThumbnail()) binding.cover.loadAny(item.manga) {
.diskCacheStrategy(DiskCacheStrategy.RESOURCE) transformations(RoundedCornersTransformation(radius))
.apply(requestOptions) }
.into(binding.cover)
} }
} }

View File

@ -2,13 +2,10 @@ package eu.kanade.tachiyomi.ui.recent.updates
import android.view.View import android.view.View
import androidx.core.view.isVisible import androidx.core.view.isVisible
import com.bumptech.glide.load.engine.DiskCacheStrategy import coil.clear
import com.bumptech.glide.load.resource.bitmap.CenterCrop import coil.loadAny
import com.bumptech.glide.load.resource.bitmap.RoundedCorners import coil.transform.RoundedCornersTransformation
import com.bumptech.glide.request.RequestOptions
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.glide.GlideApp
import eu.kanade.tachiyomi.data.glide.toMangaThumbnail
import eu.kanade.tachiyomi.databinding.UpdatesItemBinding import eu.kanade.tachiyomi.databinding.UpdatesItemBinding
import eu.kanade.tachiyomi.source.LocalSource import eu.kanade.tachiyomi.source.LocalSource
import eu.kanade.tachiyomi.ui.manga.chapter.base.BaseChapterHolder import eu.kanade.tachiyomi.ui.manga.chapter.base.BaseChapterHolder
@ -58,15 +55,10 @@ class UpdatesHolder(private val view: View, private val adapter: UpdatesAdapter)
binding.download.setState(item.status, item.progress) binding.download.setState(item.status, item.progress)
// Set cover // Set cover
GlideApp.with(itemView.context).clear(binding.mangaCover) val radius = itemView.context.resources.getDimension(R.dimen.card_radius)
binding.mangaCover.clear()
val radius = itemView.context.resources.getDimensionPixelSize(R.dimen.card_radius) binding.mangaCover.loadAny(item.manga) {
val requestOptions = RequestOptions().transform(CenterCrop(), RoundedCorners(radius)) transformations(RoundedCornersTransformation(radius))
GlideApp.with(itemView.context) }
.load(item.manga.toMangaThumbnail())
.diskCacheStrategy(DiskCacheStrategy.RESOURCE)
.apply(requestOptions)
.dontAnimate()
.into(binding.mangaCover)
} }
} }

View File

@ -3,61 +3,41 @@ package eu.kanade.tachiyomi.widget
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
import android.view.View import android.view.View
import android.widget.ImageView import android.widget.ImageView
import android.widget.ImageView.ScaleType
import androidx.appcompat.content.res.AppCompatResources
import androidx.core.view.isVisible import androidx.core.view.isVisible
import com.bumptech.glide.request.target.ImageViewTarget import coil.drawable.CrossfadeDrawable
import com.bumptech.glide.request.transition.Transition import coil.target.ImageViewTarget
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.util.system.getResourceColor
/** /**
* A glide target to display an image with an optional view to show while loading and a configurable * A Coil target to display an image with an optional view to show while loading.
* error drawable.
* *
* @param view the view where the image will be loaded * @param target the view where the image will be loaded
* @param progress an optional view to show when the image is loading. * @param progress the view to show when the image is loading.
* @param errorDrawableRes the error drawable resource to show. * @param crossfadeDuration duration in millisecond to crossfade the result drawable
* @param errorScaleType the scale type for the error drawable, [ScaleType.CENTER] by default.
*/ */
class StateImageViewTarget( class StateImageViewTarget(
view: ImageView, private val target: ImageView,
val progress: View? = null, private val progress: View,
private val errorDrawableRes: Int = R.drawable.ic_broken_image_grey_24dp, private val crossfadeDuration: Int = 0
private val errorScaleType: ScaleType = ScaleType.CENTER ) : ImageViewTarget(target) {
) : ImageViewTarget<Drawable>(view) { override fun onStart(placeholder: Drawable?) {
progress.isVisible = true
private var resource: Drawable? = null
private val imageScaleType = view.scaleType
override fun setResource(resource: Drawable?) {
view.setImageDrawable(resource)
} }
override fun onLoadStarted(placeholder: Drawable?) { override fun onSuccess(result: Drawable) {
progress?.isVisible = true progress.isVisible = false
super.onLoadStarted(placeholder) if (crossfadeDuration > 0) {
val crossfadeResult = CrossfadeDrawable(target.drawable, result, durationMillis = crossfadeDuration)
target.setImageDrawable(crossfadeResult)
crossfadeResult.start()
} else {
target.setImageDrawable(result)
}
} }
override fun onLoadFailed(errorDrawable: Drawable?) { override fun onError(error: Drawable?) {
progress?.isVisible = false progress.isVisible = false
view.scaleType = errorScaleType if (error != null) {
target.setImageDrawable(error)
val vector = AppCompatResources.getDrawable(view.context, errorDrawableRes) }
vector?.setTint(view.context.getResourceColor(R.attr.colorOnBackground, 0.38f))
view.setImageDrawable(vector)
}
override fun onLoadCleared(placeholder: Drawable?) {
progress?.isVisible = false
super.onLoadCleared(placeholder)
}
override fun onResourceReady(resource: Drawable, transition: Transition<in Drawable>?) {
progress?.isVisible = false
view.scaleType = imageScaleType
super.onResourceReady(resource, transition)
this.resource = resource
} }
} }

View File

@ -17,6 +17,7 @@
android:layout_height="0dp" android:layout_height="0dp"
android:layout_marginBottom="44dp" android:layout_marginBottom="44dp"
android:alpha="0.2" android:alpha="0.2"
android:scaleType="centerCrop"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
@ -50,6 +51,7 @@
android:background="@drawable/rounded_rectangle" android:background="@drawable/rounded_rectangle"
android:contentDescription="@string/description_cover" android:contentDescription="@string/description_cover"
android:maxWidth="100dp" android:maxWidth="100dp"
android:scaleType="centerCrop"
tools:src="@mipmap/ic_launcher" /> tools:src="@mipmap/ic_launcher" />
<LinearLayout <LinearLayout

View File

@ -25,6 +25,7 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:background="?attr/colorSurface" android:background="?attr/colorSurface"
android:scaleType="centerCrop"
tools:ignore="ContentDescription" tools:ignore="ContentDescription"
tools:src="@mipmap/ic_launcher" /> tools:src="@mipmap/ic_launcher" />

View File

@ -17,6 +17,7 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:background="?attr/colorSurface" android:background="?attr/colorSurface"
android:scaleType="centerCrop"
tools:ignore="ContentDescription" tools:ignore="ContentDescription"
tools:src="@mipmap/ic_launcher" /> tools:src="@mipmap/ic_launcher" />

View File

@ -15,6 +15,7 @@
android:layout_height="@dimen/material_component_lists_single_line_with_avatar_height" android:layout_height="@dimen/material_component_lists_single_line_with_avatar_height"
android:layout_gravity="center_vertical" android:layout_gravity="center_vertical"
android:padding="8dp" android:padding="8dp"
android:scaleType="centerCrop"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"

View File

@ -14,6 +14,7 @@
android:layout_marginStart="16dp" android:layout_marginStart="16dp"
android:layout_marginTop="8dp" android:layout_marginTop="8dp"
android:layout_marginBottom="8dp" android:layout_marginBottom="8dp"
android:scaleType="centerCrop"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintDimensionRatio="h,1:1" app:layout_constraintDimensionRatio="h,1:1"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"