refactor: Switch to Coil3

This commit is contained in:
Ahmad Ansori Palembani 2024-05-23 09:04:02 +07:00
parent 2bffd8a653
commit 3412806bfc
Signed by: null2264
GPG key ID: BA64F8B60AF3EFB6
33 changed files with 286 additions and 233 deletions

View file

@ -256,9 +256,7 @@ dependencies {
implementation(libs.injekt.core)
// Image library
implementation(libs.coil)
implementation(libs.coil.gif)
implementation(libs.coil.svg)
implementation(libs.bundles.coil)
// Logging
implementation(libs.timber)
@ -327,7 +325,7 @@ tasks {
"-opt-in=androidx.compose.foundation.ExperimentalFoundationApi",
"-opt-in=androidx.compose.animation.ExperimentalAnimationApi",
"-opt-in=androidx.compose.animation.graphics.ExperimentalAnimationGraphicsApi",
"-opt-in=coil.annotation.ExperimentalCoilApi",
"-opt-in=coil3.annotation.ExperimentalCoilApi",
"-opt-in=com.google.accompanist.permissions.ExperimentalPermissionsApi",
"-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi",
"-opt-in=kotlinx.coroutines.FlowPreview",

View file

@ -20,6 +20,9 @@ import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.ProcessLifecycleOwner
import androidx.lifecycle.lifecycleScope
import androidx.multidex.MultiDex
import coil3.ImageLoader
import coil3.PlatformContext
import coil3.SingletonImageLoader
import dev.yokai.domain.AppState
import eu.kanade.tachiyomi.appwidget.TachiyomiWidgetManager
import eu.kanade.tachiyomi.data.image.coil.CoilSetup
@ -44,7 +47,7 @@ import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.injectLazy
import java.security.Security
open class App : Application(), DefaultLifecycleObserver {
open class App : Application(), DefaultLifecycleObserver, SingletonImageLoader.Factory {
val preferences: PreferencesHelper by injectLazy()
@ -71,7 +74,6 @@ open class App : Application(), DefaultLifecycleObserver {
Injekt.importModule(PreferenceModule(this))
Injekt.importModule(AppModule(this))
CoilSetup(this)
setupNotificationChannels()
ProcessLifecycleOwner.get().lifecycle.addObserver(this)
@ -171,6 +173,10 @@ open class App : Application(), DefaultLifecycleObserver {
}
}
}
override fun newImageLoader(context: PlatformContext): ImageLoader {
return CoilSetup.setup(context)
}
}
private const val ACTION_DISABLE_INCOGNITO_MODE = "tachi.action.DISABLE_INCOGNITO_MODE"

View file

@ -16,13 +16,14 @@ import androidx.glance.appwidget.provideContent
import androidx.glance.appwidget.updateAll
import androidx.glance.background
import androidx.glance.layout.fillMaxSize
import coil.executeBlocking
import coil.imageLoader
import coil.request.CachePolicy
import coil.request.ImageRequest
import coil.size.Precision
import coil.size.Scale
import coil.transform.RoundedCornersTransformation
import coil3.executeBlocking
import coil3.imageLoader
import coil3.request.CachePolicy
import coil3.request.ImageRequest
import coil3.request.transformations
import coil3.size.Precision
import coil3.size.Scale
import coil3.transform.RoundedCornersTransformation
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.appwidget.components.CoverHeight
import eu.kanade.tachiyomi.appwidget.components.CoverWidth
@ -109,7 +110,7 @@ class UpdatesGridGlanceWidget : GlanceAppWidget() {
}
}
.build()
Pair(updatesView.id!!, app.imageLoader.executeBlocking(request).drawable?.toBitmap())
Pair(updatesView.id!!, app.imageLoader.executeBlocking(request).image?.asDrawable(app.resources)?.toBitmap())
}
}

View file

@ -2,8 +2,8 @@ package eu.kanade.tachiyomi.data.cache
import android.content.Context
import android.text.format.Formatter
import coil.imageLoader
import coil.memory.MemoryCache
import coil3.imageLoader
import coil3.memory.MemoryCache
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.data.database.models.Manga

View file

@ -2,47 +2,38 @@ package eu.kanade.tachiyomi.data.image.coil
import android.app.ActivityManager
import android.content.Context
import android.os.Build
import androidx.core.content.getSystemService
import coil.Coil
import coil.ImageLoader
import coil.decode.GifDecoder
import coil.decode.ImageDecoderDecoder
import coil.disk.DiskCache
import coil.memory.MemoryCache
import coil.util.DebugLogger
import eu.kanade.tachiyomi.BuildConfig
import coil3.ImageLoader
import coil3.disk.DiskCache
import coil3.disk.directory
import coil3.memory.MemoryCache
import coil3.network.okhttp.OkHttpNetworkFetcherFactory
import coil3.request.allowHardware
import coil3.request.allowRgb565
import coil3.request.crossfade
import eu.kanade.tachiyomi.network.NetworkHelper
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
class CoilSetup(context: Context) {
init {
val imageLoader = ImageLoader.Builder(context).apply {
val callFactoryInit = { Injekt.get<NetworkHelper>().client }
val diskCacheInit = { CoilDiskCache.get(context) }
components {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
add(ImageDecoderDecoder.Factory())
} else {
add(GifDecoder.Factory())
class CoilSetup {
companion object {
fun setup(context: Context): ImageLoader {
return ImageLoader.Builder(context).apply {
val callFactoryLazy = lazy { Injekt.get<NetworkHelper>().client }
val diskCacheLazy = lazy { CoilDiskCache.get(context) }
components {
add(OkHttpNetworkFetcherFactory(callFactoryLazy::value))
add(TachiyomiImageDecoder.Factory())
add(MangaCoverFetcher.Factory(callFactoryLazy, diskCacheLazy))
add(MangaCoverKeyer())
}
add(TachiyomiImageDecoder.Factory())
add(MangaCoverFetcher.Factory(lazy(callFactoryInit), lazy(diskCacheInit)))
add(MangaCoverKeyer())
add(InputStreamFetcher.Factory())
}
callFactory(callFactoryInit)
diskCache(diskCacheInit)
memoryCache { MemoryCache.Builder(context).maxSizePercent(0.40).build() }
crossfade(true)
allowRgb565(context.getSystemService<ActivityManager>()!!.isLowRamDevice)
allowHardware(true)
if (BuildConfig.DEBUG) {
logger(DebugLogger())
}
}.build()
Coil.setImageLoader(imageLoader)
diskCache(diskCacheLazy::value)
memoryCache { MemoryCache.Builder().maxSizePercent(context, 0.40).build() }
crossfade(true)
allowRgb565(context.getSystemService<ActivityManager>()!!.isLowRamDevice)
allowHardware(true)
}.build()
}
}
}

View file

@ -5,7 +5,8 @@ import android.view.View
import android.widget.ImageView
import androidx.core.view.isVisible
import androidx.vectordrawable.graphics.drawable.VectorDrawableCompat
import coil.target.ImageViewTarget
import coil3.Image
import coil3.target.ImageViewTarget
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.util.system.getResourceColor
@ -15,7 +16,9 @@ class CoverViewTarget(
val scaleType: ImageView.ScaleType = ImageView.ScaleType.CENTER_CROP,
) : ImageViewTarget(view) {
override fun onError(error: Drawable?) {
override fun onError(error: Image?) {
//val drawable = error?.asDrawable(view.context.resources)
progress?.isVisible = false
view.scaleType = ImageView.ScaleType.CENTER
val vector = VectorDrawableCompat.create(
@ -27,13 +30,17 @@ class CoverViewTarget(
view.setImageDrawable(vector)
}
override fun onStart(placeholder: Drawable?) {
override fun onStart(placeholder: Image?) {
//val drawable = placeholder?.asDrawable(view.context.resources)
progress?.isVisible = true
view.scaleType = scaleType
super.onStart(placeholder)
}
override fun onSuccess(result: Drawable) {
override fun onSuccess(result: Image) {
//val drawable = result?.asDrawable(view.context.resources)
progress?.isVisible = false
view.scaleType = scaleType
super.onSuccess(result)

View file

@ -1,15 +1,15 @@
package eu.kanade.tachiyomi.data.image.coil
import android.graphics.BitmapFactory
import android.graphics.drawable.Drawable
import android.widget.ImageView
import androidx.palette.graphics.Palette
import coil.ImageLoader
import coil.imageLoader
import coil.memory.MemoryCache
import coil.request.Disposable
import coil.request.ImageRequest
import coil.target.ImageViewTarget
import coil3.Image
import coil3.ImageLoader
import coil3.imageLoader
import coil3.memory.MemoryCache
import coil3.request.Disposable
import coil3.request.ImageRequest
import coil3.target.ImageViewTarget
import eu.kanade.tachiyomi.data.cache.CoverCache
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.util.system.launchIO
@ -22,7 +22,7 @@ class LibraryMangaImageTarget(
private val coverCache: CoverCache by injectLazy()
override fun onError(error: Drawable?) {
override fun onError(error: Image?) {
super.onError(error)
if (manga.favorite) {
launchIO {

View file

@ -1,16 +1,16 @@
package eu.kanade.tachiyomi.data.image.coil
import android.webkit.MimeTypeMap
import coil.ImageLoader
import coil.decode.DataSource
import coil.decode.ImageSource
import coil.disk.DiskCache
import coil.fetch.FetchResult
import coil.fetch.Fetcher
import coil.fetch.SourceResult
import coil.network.HttpException
import coil.request.Options
import coil.request.Parameters
import coil3.Extras
import coil3.ImageLoader
import coil3.decode.DataSource
import coil3.decode.ImageSource
import coil3.disk.DiskCache
import coil3.fetch.FetchResult
import coil3.fetch.Fetcher
import coil3.fetch.SourceFetchResult
import coil3.getOrDefault
import coil3.request.Options
import eu.kanade.tachiyomi.data.cache.CoverCache
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.network.await
@ -26,17 +26,16 @@ import okhttp3.Call
import okhttp3.Request
import okhttp3.Response
import okhttp3.internal.closeQuietly
import okio.FileSystem
import okio.Path.Companion.toOkioPath
import okio.Source
import okio.buffer
import okio.sink
import okio.source
import timber.log.Timber
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
import java.io.File
import java.net.HttpURLConnection
import java.util.Date
import java.util.*
class MangaCoverFetcher(
private val manga: Manga,
@ -71,7 +70,7 @@ class MangaCoverFetcher(
val networkRead = options.networkCachePolicy.readEnabled
val onlyCache = !networkRead && diskRead
val shouldFetchRemotely = networkRead && !diskRead && !onlyCache
val useCustomCover = options.parameters.value(useCustomCover) ?: true
val useCustomCover = options.extras.getOrDefault(USE_CUSTOM_COVER_KEY)
// Use custom cover if exists
if (!shouldFetchRemotely) {
val customCoverFile by lazy { coverCache.getCustomCoverFile(manga) }
@ -101,7 +100,7 @@ class MangaCoverFetcher(
// Read from snapshot
setRatioAndColorsInScope(manga)
return SourceResult(
return SourceFetchResult(
source = snapshot.toImageSource(),
mimeType = "image/*",
dataSource = DataSource.DISK,
@ -122,7 +121,7 @@ class MangaCoverFetcher(
// Read from disk cache
snapshot = writeToDiskCache(snapshot, response)
if (snapshot != null) {
return SourceResult(
return SourceFetchResult(
source = snapshot.toImageSource(),
mimeType = "image/*",
dataSource = DataSource.NETWORK,
@ -130,8 +129,8 @@ class MangaCoverFetcher(
}
// Read from response if cache is unused or unusable
return SourceResult(
source = ImageSource(source = responseBody.source(), context = options.context),
return SourceFetchResult(
source = ImageSource(source = responseBody.source(), fileSystem = FileSystem.SYSTEM),
mimeType = "image/*",
dataSource = if (response.cacheResponse != null) DataSource.DISK else DataSource.NETWORK,
)
@ -149,18 +148,20 @@ class MangaCoverFetcher(
val client = sourceLazy.value?.client ?: callFactoryLazy.value
val response = client.newCall(newRequest()).await()
if (!response.isSuccessful && response.code != HttpURLConnection.HTTP_NOT_MODIFIED) {
response.body?.closeQuietly()
throw HttpException(response)
response.body.closeQuietly()
throw Exception(response.message) // FIXME: Should probably use something else other than generic Exception
}
return response
}
private fun newRequest(): Request {
val request = Request.Builder()
.url(url)
.headers(sourceLazy.value?.headers ?: options.headers)
// Support attaching custom data to the network request.
.tag(Parameters::class.java, options.parameters)
val request = Request.Builder().apply {
url(url)
val sourceHeaders = sourceLazy.value?.headers
if (sourceHeaders != null)
headers(sourceHeaders)
}
val diskRead = options.diskCachePolicy.readEnabled
val networkRead = options.networkCachePolicy.readEnabled
@ -227,7 +228,11 @@ class MangaCoverFetcher(
}
private fun readFromDiskCache(): DiskCache.Snapshot? {
return if (options.diskCachePolicy.readEnabled) diskCacheLazy.value[diskCacheKey!!] else null
return if (options.diskCachePolicy.readEnabled) {
diskCacheLazy.value.openSnapshot(diskCacheKey!!)
} else {
null
}
}
private fun writeToDiskCache(
@ -239,15 +244,15 @@ class MangaCoverFetcher(
return null
}
val editor = if (snapshot != null) {
snapshot.closeAndEdit()
snapshot.closeAndOpenEditor()
} else {
diskCacheLazy.value.edit(diskCacheKey!!)
diskCacheLazy.value.openEditor(diskCacheKey!!)
} ?: return null
try {
diskCacheLazy.value.fileSystem.write(editor.data) {
response.body!!.source().readAll(this)
response.body.source().readAll(this)
}
return editor.commitAndGet()
return editor.commitAndOpenSnapshot()
} catch (e: Exception) {
try {
editor.abort()
@ -258,7 +263,12 @@ class MangaCoverFetcher(
}
private fun DiskCache.Snapshot.toImageSource(): ImageSource {
return ImageSource(file = data, diskCacheKey = diskCacheKey, closeable = this)
return ImageSource(
file = data,
fileSystem = FileSystem.SYSTEM,
diskCacheKey = diskCacheKey,
closeable = this,
)
}
private fun setRatioAndColorsInScope(manga: Manga, ogFile: File? = null, force: Boolean = false) {
@ -283,8 +293,12 @@ class MangaCoverFetcher(
}
private fun fileLoader(file: File): FetchResult {
return SourceResult(
source = ImageSource(file = file.toOkioPath(), diskCacheKey = diskCacheKey),
return SourceFetchResult(
source = ImageSource(
file = file.toOkioPath(),
fileSystem = FileSystem.SYSTEM,
diskCacheKey = diskCacheKey,
),
mimeType = "image/*",
dataSource = DataSource.DISK,
)
@ -318,7 +332,7 @@ class MangaCoverFetcher(
}
companion object {
const val useCustomCover = "use_custom_cover"
val USE_CUSTOM_COVER_KEY = Extras.Key(true)
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,7 +1,7 @@
package eu.kanade.tachiyomi.data.image.coil
import coil.key.Keyer
import coil.request.Options
import coil3.key.Keyer
import coil3.request.Options
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.util.storage.DiskUtil

View file

@ -2,11 +2,15 @@ package eu.kanade.tachiyomi.data.image.coil
import android.graphics.Bitmap
import android.os.Build
import androidx.core.graphics.drawable.toDrawable
import coil.ImageLoader
import coil.decode.*
import coil.fetch.SourceResult
import coil.request.Options
import coil3.ImageLoader
import coil3.asCoilImage
import coil3.decode.DecodeResult
import coil3.decode.DecodeUtils
import coil3.decode.Decoder
import coil3.decode.ImageSource
import coil3.fetch.SourceFetchResult
import coil3.request.Options
import coil3.request.bitmapConfig
import eu.kanade.tachiyomi.util.system.GLUtil
import eu.kanade.tachiyomi.util.system.ImageUtil
import okio.BufferedSource
@ -79,16 +83,16 @@ class TachiyomiImageDecoder(private val resources: ImageSource, private val opti
*/
return DecodeResult(
drawable = bitmap.toDrawable(options.context.resources),
image = bitmap.asCoilImage(),
isSampled = sampleSize > 1,
)
}
class Factory : Decoder.Factory {
override fun create(result: SourceResult, options: Options, imageLoader: ImageLoader): Decoder? {
if (isApplicable(result.source.source()) || options.customDecoder) return TachiyomiImageDecoder(result.source, options)
return null
override fun create(result: SourceFetchResult, options: Options, imageLoader: ImageLoader): Decoder? {
if (!isApplicable(result.source.source())) return null
return TachiyomiImageDecoder(result.source, options)
}
private fun isApplicable(source: BufferedSource): Boolean {

View file

@ -1,14 +1,14 @@
package eu.kanade.tachiyomi.data.image.coil
import android.graphics.Bitmap
import android.os.Build
import coil.request.ImageRequest
import coil.request.Options
import coil.size.Dimension
import coil.size.Scale
import coil.size.Size
import coil.size.isOriginal
import coil.size.pxOrElse
import coil3.Extras
import coil3.getExtra
import coil3.request.ImageRequest
import coil3.request.Options
import coil3.size.Dimension
import coil3.size.Scale
import coil3.size.Size
import coil3.size.isOriginal
import coil3.size.pxOrElse
internal inline fun Size.widthPx(scale: Scale, original: () -> Int): Int {
return if (isOriginal) original() else width.toPx(scale)
@ -26,22 +26,19 @@ internal fun Dimension.toPx(scale: Scale): Int = pxOrElse {
}
fun ImageRequest.Builder.cropBorders(enable: Boolean) = apply {
setParameter(cropBordersKey, enable)
extras[cropBordersKey] = enable
}
val Options.cropBorders: Boolean
get() = parameters.value(cropBordersKey) ?: false
get() = getExtra(cropBordersKey)
private val cropBordersKey = "crop_borders"
private val cropBordersKey = Extras.Key(default = false)
fun ImageRequest.Builder.customDecoder(enable: Boolean) = apply {
setParameter(customDecoderKey, enable)
extras[customDecoderKey] = enable
}
val Options.customDecoder: Boolean
get() = parameters.value(customDecoderKey) ?: false
get() = getExtra(customDecoderKey)
private val customDecoderKey = "custom_decoder"
val Options.bitmapConfig: Bitmap.Config
get() = this.config
private val customDecoderKey = Extras.Key(default = false)

View file

@ -16,9 +16,9 @@ import androidx.work.WorkInfo
import androidx.work.WorkManager
import androidx.work.WorkQuery
import androidx.work.WorkerParameters
import coil.Coil
import coil.request.CachePolicy
import coil.request.ImageRequest
import coil3.imageLoader
import coil3.request.CachePolicy
import coil3.request.ImageRequest
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.cache.CoverCache
import eu.kanade.tachiyomi.data.database.DatabaseHelper
@ -261,21 +261,19 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
val thumbnailUrl = manga.thumbnail_url
manga.copyFrom(networkManga)
manga.initialized = true
if (thumbnailUrl != manga.thumbnail_url) {
coverCache.deleteFromCache(thumbnailUrl)
// load new covers in background
val request =
val request: ImageRequest =
if (thumbnailUrl != manga.thumbnail_url) {
coverCache.deleteFromCache(thumbnailUrl)
// load new covers in background
ImageRequest.Builder(context).data(manga)
.memoryCachePolicy(CachePolicy.DISABLED).build()
Coil.imageLoader(context).execute(request)
} else {
val request =
} else {
ImageRequest.Builder(context).data(manga)
.memoryCachePolicy(CachePolicy.DISABLED)
.diskCachePolicy(CachePolicy.WRITE_ONLY)
.build()
Coil.imageLoader(context).execute(request)
}
}
context.imageLoader.execute(request)
db.insertManga(manga).executeAsBlocking()
}
}

View file

@ -13,10 +13,11 @@ import androidx.core.app.ActivityCompat
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import androidx.core.content.ContextCompat
import coil.Coil
import coil.request.CachePolicy
import coil.request.ImageRequest
import coil.transform.CircleCropTransformation
import coil3.imageLoader
import coil3.request.CachePolicy
import coil3.request.ImageRequest
import coil3.request.transformations
import coil3.transform.CircleCropTransformation
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.data.database.models.LibraryManga
@ -191,11 +192,11 @@ class LibraryUpdateNotifier(private val context: Context) {
.transformations(CircleCropTransformation())
.size(width = ICON_SIZE, height = ICON_SIZE).build()
Coil.imageLoader(context)
.execute(request).drawable?.let { drawable ->
context.imageLoader
.execute(request).image?.asDrawable(context.resources)?.let { drawable ->
setLargeIcon((drawable as? BitmapDrawable)?.bitmap)
}
} catch (e: Exception) {
} catch (_: Exception) {
}
setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_SUMMARY)
setContentTitle(manga.title)

View file

@ -9,8 +9,8 @@ import androidx.core.text.color
import androidx.core.text.scale
import androidx.core.view.isGone
import androidx.core.view.isVisible
import coil.dispose
import coil.load
import coil3.dispose
import coil3.load
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.image.coil.CoverViewTarget
import eu.kanade.tachiyomi.databinding.ExtensionCardItemBinding

View file

@ -10,9 +10,9 @@ import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.view.isVisible
import androidx.core.view.marginBottom
import androidx.core.view.updateLayoutParams
import coil.dispose
import coil.size.Precision
import coil.size.Scale
import coil3.dispose
import coil3.size.Precision
import coil3.size.Scale
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.image.coil.loadManga

View file

@ -4,7 +4,7 @@ import android.view.View
import android.view.ViewGroup
import androidx.core.view.isVisible
import androidx.core.view.updateLayoutParams
import coil.dispose
import coil3.dispose
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.image.coil.loadManga
import eu.kanade.tachiyomi.databinding.MangaListItemBinding

View file

@ -13,8 +13,7 @@ import android.view.inputmethod.InputMethodManager
import androidx.core.graphics.ColorUtils
import androidx.core.view.children
import androidx.core.view.isVisible
import coil.load
import coil.request.Parameters
import coil3.load
import com.google.android.material.chip.Chip
import com.google.android.material.chip.ChipGroup
import eu.kanade.tachiyomi.R
@ -215,10 +214,9 @@ class EditMangaDialog : DialogController {
binding.resetCover.setOnClickListener {
binding.mangaCover.load(
manga,
builder = {
parameters(Parameters.Builder().set(MangaCoverFetcher.useCustomCover, false).build())
},
)
) {
extras[MangaCoverFetcher.USE_CUSTOM_COVER_KEY] = false
}
customCoverUri = null
willResetCover = true
}

View file

@ -7,6 +7,7 @@ import android.app.PendingIntent
import android.content.ClipData
import android.content.Context
import android.content.Intent
import android.graphics.Bitmap
import android.graphics.Color
import android.graphics.drawable.BitmapDrawable
import android.graphics.drawable.Icon
@ -40,8 +41,9 @@ import androidx.palette.graphics.Palette
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import coil.imageLoader
import coil.request.ImageRequest
import coil3.imageLoader
import coil3.request.ImageRequest
import coil3.request.allowHardware
import com.bluelinelabs.conductor.ControllerChangeHandler
import com.bluelinelabs.conductor.ControllerChangeType
import com.google.android.material.chip.Chip
@ -569,8 +571,20 @@ class MangaDetailsController :
val request = ImageRequest.Builder(view.context).data(presenter.manga).allowHardware(false)
.memoryCacheKey(presenter.manga.key())
.target(
onSuccess = { drawable ->
val bitmap = (drawable as? BitmapDrawable)?.bitmap
onSuccess = { image ->
val drawable = image.asDrawable(view.context.resources)
val copy = (drawable as? BitmapDrawable)?.let {
BitmapDrawable(
view.context.resources,
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
it.bitmap.copy(Bitmap.Config.HARDWARE, false)
else
it.bitmap.copy(it.bitmap.config, false),
)
} ?: drawable
val bitmap = (copy as? BitmapDrawable)?.bitmap
// Generate the Palette on a background thread.
if (bitmap != null) {
Palette.from(bitmap).generate {
@ -590,7 +604,7 @@ class MangaDetailsController :
}
}
}
binding.mangaCoverFull.setImageDrawable(drawable)
binding.mangaCoverFull.setImageDrawable(copy)
getHeader()?.updateCover(manga!!)
},
onError = {

View file

@ -4,12 +4,11 @@ import android.app.Application
import android.graphics.Bitmap
import android.net.Uri
import android.os.Environment
import coil.Coil
import coil.imageLoader
import coil.memory.MemoryCache
import coil.request.CachePolicy
import coil.request.ImageRequest
import coil.request.SuccessResult
import coil3.imageLoader
import coil3.memory.MemoryCache
import coil3.request.CachePolicy
import coil3.request.ImageRequest
import coil3.request.SuccessResult
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.cache.CoverCache
import eu.kanade.tachiyomi.data.database.DatabaseHelper
@ -379,7 +378,7 @@ class MangaDetailsPresenter(
.diskCachePolicy(CachePolicy.WRITE_ONLY)
.build()
if (Coil.imageLoader(preferences.context).execute(request) is SuccessResult) {
if (preferences.context.imageLoader.execute(request) is SuccessResult) {
preferences.context.imageLoader.memoryCache?.remove(MemoryCache.Key(manga.key()))
withContext(Dispatchers.Main) {
view?.setPaletteColor()

View file

@ -25,7 +25,9 @@ import androidx.core.view.updateLayoutParams
import androidx.core.widget.TextViewCompat
import androidx.transition.TransitionSet
import androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat
import coil.request.CachePolicy
import coil3.request.CachePolicy
import coil3.request.placeholder
import coil3.request.error
import com.google.android.material.button.MaterialButton
import com.google.android.material.chip.Chip
import eu.kanade.tachiyomi.R
@ -289,7 +291,7 @@ class MangaHeaderHolder(
}
}
@SuppressLint("SetTextI18n")
@SuppressLint("SetTextI18n", "StringFormatInvalid")
fun bind(item: MangaHeaderItem, manga: Manga) {
val presenter = adapter.delegate.mangaPresenter()
if (binding == null) {
@ -680,9 +682,10 @@ class MangaHeaderHolder(
diskCachePolicy(CachePolicy.READ_ONLY)
target(
onSuccess = {
val bitmap = (it as? BitmapDrawable)?.bitmap
val result = it.asDrawable(itemView.resources)
val bitmap = (result as? BitmapDrawable)?.bitmap
if (bitmap == null) {
binding.backdrop.setImageDrawable(it)
binding.backdrop.setImageDrawable(result)
return@target
}
val yOffset = (bitmap.height / 2 * 0.33).toInt()

View file

@ -2,8 +2,9 @@ package eu.kanade.tachiyomi.ui.manga.track
import android.view.View
import androidx.core.view.isVisible
import coil.dispose
import coil.load
import coil3.dispose
import coil3.load
import coil3.request.allowHardware
import com.google.android.material.shape.CornerFamily
import com.mikepenz.fastadapter.FastAdapter
import com.mikepenz.fastadapter.items.AbstractItem

View file

@ -2,7 +2,7 @@ package eu.kanade.tachiyomi.ui.migration
import android.view.View
import androidx.recyclerview.widget.RecyclerView
import coil.dispose
import coil3.dispose
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.IFlexible
import eu.kanade.tachiyomi.data.image.coil.loadManga

View file

@ -5,8 +5,8 @@ import androidx.appcompat.widget.PopupMenu
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.view.isInvisible
import androidx.core.view.isVisible
import coil.Coil
import coil.request.ImageRequest
import coil3.imageLoader
import coil3.request.ImageRequest
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.data.database.models.Manga
@ -20,6 +20,7 @@ import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder
import eu.kanade.tachiyomi.ui.library.setFreeformCoverRatio
import eu.kanade.tachiyomi.ui.manga.MangaDetailsController
import eu.kanade.tachiyomi.util.system.launchUI
import eu.kanade.tachiyomi.util.system.setExtras
import eu.kanade.tachiyomi.util.view.setCards
import eu.kanade.tachiyomi.util.view.setVectorCompat
import eu.kanade.tachiyomi.util.view.withFadeTransaction
@ -147,9 +148,9 @@ class MigrationProcessHolder(
val request = ImageRequest.Builder(view.context).data(manga)
.target(CoverViewTarget(coverThumbnail, progress))
.setParameter(MangaCoverFetcher.useCustomCover, false)
.setExtras(MangaCoverFetcher.USE_CUSTOM_COVER_KEY, false)
.build()
Coil.imageLoader(view.context).enqueue(request)
view.context.imageLoader.enqueue(request)
compactTitle.isVisible = true
gradient.isVisible = true

View file

@ -5,9 +5,9 @@ import android.graphics.Bitmap
import android.graphics.drawable.BitmapDrawable
import androidx.core.app.NotificationCompat
import androidx.core.content.ContextCompat
import coil.Coil
import coil.request.CachePolicy
import coil.request.ImageRequest
import coil3.imageLoader
import coil3.request.CachePolicy
import coil3.request.ImageRequest
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.notification.NotificationHandler
import eu.kanade.tachiyomi.data.notification.NotificationReceiver
@ -51,7 +51,7 @@ class SaveImageNotifier(private val context: Context) {
}
},
).build()
Coil.imageLoader(context).enqueue(request)
context.imageLoader.enqueue(request)
}
private fun showCompleteNotification(file: File, image: Bitmap) {

View file

@ -18,12 +18,14 @@ import androidx.annotation.CallSuper
import androidx.annotation.StyleRes
import androidx.appcompat.widget.AppCompatImageView
import androidx.core.view.isVisible
import coil.dispose
import coil.imageLoader
import coil.request.CachePolicy
import coil.request.ImageRequest
import coil.size.Precision
import coil.size.ViewSizeResolver
import coil3.BitmapImage
import coil3.dispose
import coil3.imageLoader
import coil3.request.CachePolicy
import coil3.request.ImageRequest
import coil3.request.crossfade
import coil3.size.Precision
import coil3.size.ViewSizeResolver
import com.davemorrissey.labs.subscaleview.ImageSource
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView.SCALE_TYPE_CENTER_INSIDE
@ -180,7 +182,7 @@ open class ReaderPageImageView @JvmOverloads constructor(
}
private fun setNonAnimatedImage(
image: Any,
data: Any,
config: Config,
) = (pageView as? SubsamplingScaleImageView)?.apply {
setDoubleTapZoomDuration(config.zoomDuration.getSystemScaledDuration())
@ -226,13 +228,13 @@ open class ReaderPageImageView @JvmOverloads constructor(
val useCoilPipeline = false // FIXME: "Bitmap too large to be uploaded into a texture"
if (isWebtoon && useCoilPipeline) {
val request = ImageRequest.Builder(context)
.data(image)
.data(data)
.memoryCachePolicy(CachePolicy.DISABLED)
.diskCachePolicy(CachePolicy.DISABLED)
.target(
onSuccess = { result ->
val drawable = result as BitmapDrawable
setImage(ImageSource.bitmap(drawable.bitmap))
val image = result as BitmapDrawable
setImage(ImageSource.bitmap(image.bitmap))
isVisible = true
},
onError = {
@ -247,10 +249,10 @@ open class ReaderPageImageView @JvmOverloads constructor(
.build()
context.imageLoader.enqueue(request)
} else {
when (image) {
is BitmapDrawable -> setImage(ImageSource.bitmap(image.bitmap))
is InputStream -> setImage(ImageSource.inputStream(image))
else -> throw IllegalArgumentException("Not implemented for class ${image::class.simpleName}")
when (data) {
is BitmapDrawable -> setImage(ImageSource.bitmap(data.bitmap))
is InputStream -> setImage(ImageSource.inputStream(data))
else -> throw IllegalArgumentException("Not implemented for class ${data::class.simpleName}")
}
isVisible = true
}
@ -314,8 +316,9 @@ open class ReaderPageImageView @JvmOverloads constructor(
.diskCachePolicy(CachePolicy.DISABLED)
.target(
onSuccess = { result ->
setImageDrawable(result)
(result as? Animatable)?.start()
val drawable = result.asDrawable(context.resources)
setImageDrawable(drawable)
(drawable as? Animatable)?.start()
isVisible = true
this@ReaderPageImageView.onImageLoaded()
},

View file

@ -5,9 +5,9 @@ import android.view.View
import androidx.core.graphics.ColorUtils
import androidx.core.view.isVisible
import androidx.recyclerview.widget.RecyclerView
import coil.Coil
import coil.dispose
import coil.request.ImageRequest
import coil3.dispose
import coil3.imageLoader
import coil3.request.ImageRequest
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.IFlexible
import eu.kanade.tachiyomi.data.database.models.Manga
@ -15,6 +15,7 @@ import eu.kanade.tachiyomi.data.image.coil.CoverViewTarget
import eu.kanade.tachiyomi.data.image.coil.MangaCoverFetcher
import eu.kanade.tachiyomi.databinding.MangaGridItemBinding
import eu.kanade.tachiyomi.ui.library.LibraryCategoryAdapter
import eu.kanade.tachiyomi.util.system.setExtras
import eu.kanade.tachiyomi.util.view.setCards
/**
@ -67,9 +68,9 @@ class BrowseSourceGridHolder(
manga.id ?: return
val request = ImageRequest.Builder(view.context).data(manga)
.target(CoverViewTarget(binding.coverThumbnail, binding.progress))
.setParameter(MangaCoverFetcher.useCustomCover, false)
.setExtras(MangaCoverFetcher.USE_CUSTOM_COVER_KEY, false)
.build()
Coil.imageLoader(view.context).enqueue(request)
view.context.imageLoader.enqueue(request)
binding.coverThumbnail.alpha = if (manga.favorite) 0.34f else 1.0f
binding.card.strokeColorStateList?.defaultColor?.let { color ->

View file

@ -3,15 +3,16 @@ package eu.kanade.tachiyomi.ui.source.browse
import android.view.View
import androidx.core.view.isVisible
import androidx.recyclerview.widget.RecyclerView
import coil.Coil
import coil.dispose
import coil.request.ImageRequest
import coil3.dispose
import coil3.imageLoader
import coil3.request.ImageRequest
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.IFlexible
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.image.coil.CoverViewTarget
import eu.kanade.tachiyomi.data.image.coil.MangaCoverFetcher
import eu.kanade.tachiyomi.databinding.MangaListItemBinding
import eu.kanade.tachiyomi.util.system.setExtras
import eu.kanade.tachiyomi.util.view.setCards
/**
@ -56,9 +57,9 @@ class BrowseSourceListHolder(
manga.id ?: return
val request = ImageRequest.Builder(view.context).data(manga)
.target(CoverViewTarget(binding.coverThumbnail))
.setParameter(MangaCoverFetcher.useCustomCover, false)
.setExtras(MangaCoverFetcher.USE_CUSTOM_COVER_KEY, false)
.build()
Coil.imageLoader(view.context).enqueue(request)
view.context.imageLoader.enqueue(request)
binding.coverThumbnail.alpha = if (manga.favorite) 0.34f else 1.0f
}

View file

@ -3,16 +3,18 @@ package eu.kanade.tachiyomi.ui.source.globalsearch
import android.graphics.drawable.RippleDrawable
import android.view.View
import androidx.core.view.isVisible
import coil.Coil
import coil.dispose
import coil.request.CachePolicy
import coil.request.ImageRequest
import coil3.dispose
import coil3.imageLoader
import coil3.request.CachePolicy
import coil3.request.ImageRequest
import coil3.request.placeholder
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.image.coil.CoverViewTarget
import eu.kanade.tachiyomi.data.image.coil.MangaCoverFetcher
import eu.kanade.tachiyomi.databinding.SourceGlobalSearchControllerCardItemBinding
import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder
import eu.kanade.tachiyomi.util.system.dpToPx
import eu.kanade.tachiyomi.util.system.setExtras
import eu.kanade.tachiyomi.util.view.makeShapeCorners
import eu.kanade.tachiyomi.util.view.setCards
@ -57,9 +59,9 @@ class GlobalSearchMangaHolder(view: View, adapter: GlobalSearchCardAdapter) :
.placeholder(android.R.color.transparent)
.memoryCachePolicy(CachePolicy.DISABLED)
.target(CoverViewTarget(binding.itemImage, binding.progress))
.setParameter(MangaCoverFetcher.useCustomCover, false)
.setExtras(MangaCoverFetcher.USE_CUSTOM_COVER_KEY, false)
.build()
Coil.imageLoader(itemView.context).enqueue(request)
itemView.context.imageLoader.enqueue(request)
}
}
}

View file

@ -7,8 +7,8 @@ import android.content.pm.ShortcutManager
import android.graphics.Bitmap
import android.graphics.drawable.BitmapDrawable
import android.graphics.drawable.Icon
import coil.Coil
import coil.request.ImageRequest
import coil3.imageLoader
import coil3.request.ImageRequest
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.appwidget.TachiyomiWidgetManager
import eu.kanade.tachiyomi.data.cache.CoverCache
@ -72,8 +72,8 @@ class MangaShortcutManager(
is Manga -> {
val request = ImageRequest.Builder(context).data(item).build()
val bitmap = (
Coil.imageLoader(context)
.execute(request).drawable as? BitmapDrawable
context.imageLoader
.execute(request).image?.asDrawable(context.resources) as? BitmapDrawable
)?.bitmap
ShortcutInfo.Builder(

View file

@ -0,0 +1,9 @@
package eu.kanade.tachiyomi.util.system
import coil3.Extras
import coil3.request.ImageRequest
fun <T> ImageRequest.Builder.setExtras(extraKey: Extras.Key<T>, value: T): ImageRequest.Builder {
this.extras[extraKey] = value
return this
}

View file

@ -93,18 +93,20 @@ object ImageUtil {
}
fun isAnimatedAndSupported(stream: InputStream): Boolean {
try {
return try {
val type = getImageType(stream) ?: return false
return when (type.format) {
// https://coil-kt.github.io/coil/getting_started/#supported-image-formats
when (type.format) {
Format.Gif -> true
// Coil supports animated WebP on Android 9.0+
// https://coil-kt.github.io/coil/getting_started/#supported-image-formats
// Animated WebP on Android 9+
Format.Webp -> type.isAnimated && Build.VERSION.SDK_INT >= Build.VERSION_CODES.P
// Animated Heif on Android 11+
Format.Heif -> type.isAnimated && Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
else -> false
}
} catch (_: Exception) {
false
}
return false
}
enum class ImageType(val mime: String, val extension: String) {

View file

@ -1,20 +1,20 @@
package eu.kanade.tachiyomi.widget
import android.graphics.drawable.Drawable
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import androidx.core.view.isVisible
import coil.target.ImageViewTarget
import coil3.Image
import coil3.target.ImageViewTarget
class GifViewTarget(view: ImageView, private val progressBar: View?, private val decodeErrorLayout: ViewGroup?) : ImageViewTarget(view) {
override fun onError(error: Drawable?) {
override fun onError(error: Image?) {
progressBar?.isVisible = false
decodeErrorLayout?.isVisible = true
}
override fun onSuccess(result: Drawable) {
override fun onSuccess(result: Image) {
progressBar?.isVisible = false
decodeErrorLayout?.isVisible = false
super.onSuccess(result)

View file

@ -1,6 +1,6 @@
[versions]
chucker = "3.5.2"
coil = "2.4.0"
coil3 = "3.0.0-alpha06"
flexible-adapter = "c8013533"
fast_adapter = "5.6.0"
nucleus = "3.0.0"
@ -11,9 +11,10 @@ shizuku = "12.1.0"
accompanist-webview = { module = "com.google.accompanist:accompanist-webview", version = "0.30.1" }
chucker-library-no-op = { module = "com.github.ChuckerTeam.Chucker:library-no-op", version.ref = "chucker" }
chucker-library = { module = "com.github.ChuckerTeam.Chucker:library", version.ref = "chucker" }
coil-svg = { module = "io.coil-kt:coil-svg", version.ref = "coil" }
coil-gif = { module = "io.coil-kt:coil-gif", version.ref = "coil" }
coil = { module = "io.coil-kt:coil", version.ref = "coil" }
coil3 = { module = "io.coil-kt.coil3:coil", version.ref = "coil3" }
coil3-svg = { module = "io.coil-kt.coil3:coil-svg", version.ref = "coil3" }
coil3-gif = { module = "io.coil-kt.coil3:coil-gif", version.ref = "coil3" }
coil3-okhttp = { module = "io.coil-kt.coil3:coil-network-okhttp", version.ref = "coil3" }
compose-theme-adapter3 = { module = "com.google.accompanist:accompanist-themeadapter-material3", version = "0.33.2-alpha" }
conductor = { module = "com.bluelinelabs:conductor", version = "4.0.0-preview-4" }
conductor-support-preference = { module = "com.github.tachiyomiorg:conductor-support-preference", version = "3.0.0" }
@ -77,4 +78,5 @@ kotlinter = { id = "org.jmailen.kotlinter", version = "4.1.1" }
gradle-versions = { id = "com.github.ben-manes.versions", version = "0.42.0" }
[bundles]
coil = [ "coil3", "coil3-svg", "coil3-gif", "coil3-okhttp" ]
test = [ "junit", "mockk" ]