Migrate to coil3

master
Koitharu 2 years ago
parent 681c80dc3e
commit 436168b940
Signed by: Koitharu
GPG Key ID: 676DEE768C17A9D7

@ -59,7 +59,7 @@ android {
'-opt-in=kotlinx.coroutines.ExperimentalForInheritanceCoroutinesApi', '-opt-in=kotlinx.coroutines.ExperimentalForInheritanceCoroutinesApi',
'-opt-in=kotlinx.coroutines.FlowPreview', '-opt-in=kotlinx.coroutines.FlowPreview',
'-opt-in=kotlin.contracts.ExperimentalContracts', '-opt-in=kotlin.contracts.ExperimentalContracts',
'-opt-in=coil.annotation.ExperimentalCoilApi', '-opt-in=coil3.annotation.ExperimentalCoilApi',
] ]
} }
lint { lint {
@ -134,8 +134,9 @@ dependencies {
implementation 'androidx.hilt:hilt-work:1.2.0' implementation 'androidx.hilt:hilt-work:1.2.0'
kapt 'androidx.hilt:hilt-compiler:1.2.0' kapt 'androidx.hilt:hilt-compiler:1.2.0'
implementation 'io.coil-kt:coil-base:2.7.0' implementation 'io.coil-kt.coil3:coil-core:3.0.0-rc01'
implementation 'io.coil-kt:coil-svg:2.7.0' implementation 'io.coil-kt.coil3:coil-network-okhttp:3.0.0-rc01'
implementation 'io.coil-kt.coil3:coil-gif:3.0.0-rc01'
implementation 'org.aomedia.avif.android:avif:1.1.1.14d8e3c4' implementation 'org.aomedia.avif.android:avif:1.1.1.14d8e3c4'
implementation 'com.github.KotatsuApp:subsampling-scale-image-view:d1d10a6975' implementation 'com.github.KotatsuApp:subsampling-scale-image-view:d1d10a6975'
implementation 'com.github.solkin:disk-lru-cache:1.4' implementation 'com.github.solkin:disk-lru-cache:1.4'

@ -5,9 +5,16 @@ import androidx.core.content.ContextCompat
import androidx.core.text.buildSpannedString import androidx.core.text.buildSpannedString
import androidx.core.text.inSpans import androidx.core.text.inSpans
import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleOwner
import coil.ImageLoader import coil3.ImageLoader
import coil.request.ImageRequest import coil3.request.ImageRequest
import coil.transform.RoundedCornersTransformation import coil3.request.allowRgb565
import coil3.request.crossfade
import coil3.request.error
import coil3.request.fallback
import coil3.request.lifecycle
import coil3.request.placeholder
import coil3.request.transformations
import coil3.transform.RoundedCornersTransformation
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.model.getTitle import org.koitharu.kotatsu.core.model.getTitle
@ -19,8 +26,9 @@ import org.koitharu.kotatsu.core.ui.list.AdapterDelegateClickListenerAdapter
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.util.ext.defaultPlaceholders import org.koitharu.kotatsu.core.util.ext.defaultPlaceholders
import org.koitharu.kotatsu.core.util.ext.enqueueWith import org.koitharu.kotatsu.core.util.ext.enqueueWith
import org.koitharu.kotatsu.core.util.ext.mangaExtra
import org.koitharu.kotatsu.core.util.ext.mangaSourceExtra
import org.koitharu.kotatsu.core.util.ext.newImageRequest import org.koitharu.kotatsu.core.util.ext.newImageRequest
import org.koitharu.kotatsu.core.util.ext.source
import org.koitharu.kotatsu.databinding.ItemMangaAlternativeBinding import org.koitharu.kotatsu.databinding.ItemMangaAlternativeBinding
import org.koitharu.kotatsu.list.ui.ListModelDiffCallback import org.koitharu.kotatsu.list.ui.ListModelDiffCallback
import org.koitharu.kotatsu.list.ui.model.ListModel import org.koitharu.kotatsu.list.ui.model.ListModel
@ -74,7 +82,7 @@ fun alternativeAD(
.placeholder(R.drawable.ic_web) .placeholder(R.drawable.ic_web)
.fallback(R.drawable.ic_web) .fallback(R.drawable.ic_web)
.error(R.drawable.ic_web) .error(R.drawable.ic_web)
.source(item.manga.source) .mangaSourceExtra(item.manga.source)
.transformations(RoundedCornersTransformation(context.resources.getDimension(R.dimen.chip_icon_corner))) .transformations(RoundedCornersTransformation(context.resources.getDimension(R.dimen.chip_icon_corner)))
.allowRgb565(true) .allowRgb565(true)
.enqueueWith(coil) .enqueueWith(coil)
@ -84,8 +92,7 @@ fun alternativeAD(
defaultPlaceholders(context) defaultPlaceholders(context)
transformations(TrimTransformation()) transformations(TrimTransformation())
allowRgb565(true) allowRgb565(true)
tag(item.manga) mangaExtra(item.manga)
source(item.manga.source)
enqueueWith(coil) enqueueWith(coil)
} }
} }

@ -8,7 +8,7 @@ import android.widget.Toast
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.core.graphics.Insets import androidx.core.graphics.Insets
import androidx.core.view.updatePadding import androidx.core.view.updatePadding
import coil.ImageLoader import coil3.ImageLoader
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.exceptions.resolve.SnackbarErrorObserver import org.koitharu.kotatsu.core.exceptions.resolve.SnackbarErrorObserver

@ -12,8 +12,8 @@ import androidx.core.app.NotificationManagerCompat
import androidx.core.app.PendingIntentCompat import androidx.core.app.PendingIntentCompat
import androidx.core.app.ServiceCompat import androidx.core.app.ServiceCompat
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import coil.ImageLoader import coil3.ImageLoader
import coil.request.ImageRequest import coil3.request.ImageRequest
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
@ -23,6 +23,7 @@ import org.koitharu.kotatsu.core.model.getTitle
import org.koitharu.kotatsu.core.ui.CoroutineIntentService import org.koitharu.kotatsu.core.ui.CoroutineIntentService
import org.koitharu.kotatsu.core.util.ext.checkNotificationPermission import org.koitharu.kotatsu.core.util.ext.checkNotificationPermission
import org.koitharu.kotatsu.core.util.ext.getDisplayMessage import org.koitharu.kotatsu.core.util.ext.getDisplayMessage
import org.koitharu.kotatsu.core.util.ext.mangaSourceExtra
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
import org.koitharu.kotatsu.core.util.ext.toBitmapOrNull import org.koitharu.kotatsu.core.util.ext.toBitmapOrNull
import org.koitharu.kotatsu.details.ui.DetailsActivity import org.koitharu.kotatsu.details.ui.DetailsActivity
@ -121,7 +122,7 @@ class AutoFixService : CoroutineIntentService() {
coil.execute( coil.execute(
ImageRequest.Builder(applicationContext) ImageRequest.Builder(applicationContext)
.data(replacement.coverUrl) .data(replacement.coverUrl)
.tag(replacement.source) .mangaSourceExtra(replacement.source)
.build(), .build(),
).toBitmapOrNull(), ).toBitmapOrNull(),
) )

@ -14,7 +14,7 @@ import androidx.core.view.updateLayoutParams
import androidx.core.view.updatePadding import androidx.core.view.updatePadding
import androidx.fragment.app.viewModels import androidx.fragment.app.viewModels
import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.GridLayoutManager
import coil.ImageLoader import coil3.ImageLoader
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.bookmarks.domain.Bookmark import org.koitharu.kotatsu.bookmarks.domain.Bookmark

@ -1,17 +1,18 @@
package org.koitharu.kotatsu.bookmarks.ui.adapter package org.koitharu.kotatsu.bookmarks.ui.adapter
import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleOwner
import coil.ImageLoader import coil3.ImageLoader
import coil3.request.allowRgb565
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
import org.koitharu.kotatsu.bookmarks.domain.Bookmark import org.koitharu.kotatsu.bookmarks.domain.Bookmark
import org.koitharu.kotatsu.core.ui.image.CoverSizeResolver import org.koitharu.kotatsu.core.ui.image.CoverSizeResolver
import org.koitharu.kotatsu.core.ui.list.AdapterDelegateClickListenerAdapter import org.koitharu.kotatsu.core.ui.list.AdapterDelegateClickListenerAdapter
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.util.ext.bookmarkExtra
import org.koitharu.kotatsu.core.util.ext.decodeRegion import org.koitharu.kotatsu.core.util.ext.decodeRegion
import org.koitharu.kotatsu.core.util.ext.defaultPlaceholders import org.koitharu.kotatsu.core.util.ext.defaultPlaceholders
import org.koitharu.kotatsu.core.util.ext.enqueueWith import org.koitharu.kotatsu.core.util.ext.enqueueWith
import org.koitharu.kotatsu.core.util.ext.newImageRequest import org.koitharu.kotatsu.core.util.ext.newImageRequest
import org.koitharu.kotatsu.core.util.ext.source
import org.koitharu.kotatsu.databinding.ItemBookmarkLargeBinding import org.koitharu.kotatsu.databinding.ItemBookmarkLargeBinding
import org.koitharu.kotatsu.list.ui.model.ListModel import org.koitharu.kotatsu.list.ui.model.ListModel
@ -29,9 +30,8 @@ fun bookmarkLargeAD(
size(CoverSizeResolver(binding.imageViewThumb)) size(CoverSizeResolver(binding.imageViewThumb))
defaultPlaceholders(context) defaultPlaceholders(context)
allowRgb565(true) allowRgb565(true)
tag(item) bookmarkExtra(item)
decodeRegion(item.scroll) decodeRegion(item.scroll)
source(item.manga.source)
enqueueWith(coil) enqueueWith(coil)
} }
binding.progressView.setProgress(item.percent, false) binding.progressView.setProgress(item.percent, false)

@ -1,19 +1,21 @@
package org.koitharu.kotatsu.bookmarks.ui.adapter package org.koitharu.kotatsu.bookmarks.ui.adapter
import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleOwner
import coil.ImageLoader import coil3.ImageLoader
import coil3.request.allowRgb565
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
import org.koitharu.kotatsu.bookmarks.domain.Bookmark import org.koitharu.kotatsu.bookmarks.domain.Bookmark
import org.koitharu.kotatsu.core.ui.image.CoverSizeResolver import org.koitharu.kotatsu.core.ui.image.CoverSizeResolver
import org.koitharu.kotatsu.core.ui.list.AdapterDelegateClickListenerAdapter import org.koitharu.kotatsu.core.ui.list.AdapterDelegateClickListenerAdapter
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.util.ext.bookmarkExtra
import org.koitharu.kotatsu.core.util.ext.decodeRegion import org.koitharu.kotatsu.core.util.ext.decodeRegion
import org.koitharu.kotatsu.core.util.ext.defaultPlaceholders import org.koitharu.kotatsu.core.util.ext.defaultPlaceholders
import org.koitharu.kotatsu.core.util.ext.enqueueWith import org.koitharu.kotatsu.core.util.ext.enqueueWith
import org.koitharu.kotatsu.core.util.ext.newImageRequest import org.koitharu.kotatsu.core.util.ext.newImageRequest
import org.koitharu.kotatsu.core.util.ext.source
import org.koitharu.kotatsu.databinding.ItemBookmarkBinding import org.koitharu.kotatsu.databinding.ItemBookmarkBinding
// TODO check usages
fun bookmarkListAD( fun bookmarkListAD(
coil: ImageLoader, coil: ImageLoader,
lifecycleOwner: LifecycleOwner, lifecycleOwner: LifecycleOwner,
@ -28,9 +30,8 @@ fun bookmarkListAD(
size(CoverSizeResolver(binding.imageViewThumb)) size(CoverSizeResolver(binding.imageViewThumb))
defaultPlaceholders(context) defaultPlaceholders(context)
allowRgb565(true) allowRgb565(true)
tag(item) bookmarkExtra(item)
decodeRegion(item.scroll) decodeRegion(item.scroll)
source(item.manga.source)
enqueueWith(coil) enqueueWith(coil)
} }
} }

@ -2,7 +2,7 @@ package org.koitharu.kotatsu.bookmarks.ui.adapter
import android.content.Context import android.content.Context
import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleOwner
import coil.ImageLoader import coil3.ImageLoader
import org.koitharu.kotatsu.bookmarks.domain.Bookmark import org.koitharu.kotatsu.bookmarks.domain.Bookmark
import org.koitharu.kotatsu.core.ui.BaseListAdapter import org.koitharu.kotatsu.core.ui.BaseListAdapter
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener

@ -9,9 +9,10 @@ import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat import androidx.core.app.NotificationManagerCompat
import androidx.core.app.PendingIntentCompat import androidx.core.app.PendingIntentCompat
import androidx.core.net.toUri import androidx.core.net.toUri
import coil.EventListener import coil3.EventListener
import coil.request.ErrorResult import coil3.Extras
import coil.request.ImageRequest import coil3.request.ErrorResult
import coil3.request.ImageRequest
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.exceptions.CloudFlareProtectedException import org.koitharu.kotatsu.core.exceptions.CloudFlareProtectedException
import org.koitharu.kotatsu.core.model.getTitle import org.koitharu.kotatsu.core.model.getTitle
@ -21,7 +22,7 @@ import org.koitharu.kotatsu.parsers.model.MangaSource
class CaptchaNotifier( class CaptchaNotifier(
private val context: Context, private val context: Context,
) : EventListener { ) : EventListener() {
fun notify(exception: CloudFlareProtectedException) { fun notify(exception: CloudFlareProtectedException) {
if (!context.checkNotificationPermission(CHANNEL_ID)) { if (!context.checkNotificationPermission(CHANNEL_ID)) {
@ -84,20 +85,19 @@ class CaptchaNotifier(
override fun onError(request: ImageRequest, result: ErrorResult) { override fun onError(request: ImageRequest, result: ErrorResult) {
super.onError(request, result) super.onError(request, result)
val e = result.throwable val e = result.throwable
if (e is CloudFlareProtectedException && request.parameters.value<Boolean>(PARAM_IGNORE_CAPTCHA) != true) { if (e is CloudFlareProtectedException && request.extras[ignoreCaptchaKey] != true) {
notify(e) notify(e)
} }
} }
companion object { companion object {
fun ImageRequest.Builder.ignoreCaptchaErrors() = setParameter( fun ImageRequest.Builder.ignoreCaptchaErrors() = apply {
key = PARAM_IGNORE_CAPTCHA, extras[ignoreCaptchaKey] = true
value = true, }
memoryCacheKey = null,
) val ignoreCaptchaKey = Extras.Key(false)
private const val PARAM_IGNORE_CAPTCHA = "ignore_captcha"
private const val CHANNEL_ID = "captcha" private const val CHANNEL_ID = "captcha"
private const val TAG = CHANNEL_ID private const val TAG = CHANNEL_ID
private const val GROUP_CAPTCHA = "org.koitharu.kotatsu.CAPTCHA" private const val GROUP_CAPTCHA = "org.koitharu.kotatsu.CAPTCHA"

@ -2,16 +2,20 @@ package org.koitharu.kotatsu.core
import android.app.Application import android.app.Application
import android.content.Context import android.content.Context
import android.os.Build
import android.provider.SearchRecentSuggestions import android.provider.SearchRecentSuggestions
import android.text.Html import android.text.Html
import androidx.collection.arraySetOf import androidx.collection.arraySetOf
import androidx.room.InvalidationTracker import androidx.room.InvalidationTracker
import androidx.work.WorkManager import androidx.work.WorkManager
import coil.ComponentRegistry import coil3.ImageLoader
import coil.ImageLoader import coil3.disk.DiskCache
import coil.decode.SvgDecoder import coil3.disk.directory
import coil.disk.DiskCache import coil3.gif.AnimatedImageDecoder
import coil.util.DebugLogger import coil3.gif.GifDecoder
import coil3.network.okhttp.OkHttpNetworkFetcherFactory
import coil3.request.allowRgb565
import coil3.util.DebugLogger
import dagger.Binds import dagger.Binds
import dagger.Module import dagger.Module
import dagger.Provides import dagger.Provides
@ -28,6 +32,8 @@ import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.browser.cloudflare.CaptchaNotifier import org.koitharu.kotatsu.browser.cloudflare.CaptchaNotifier
import org.koitharu.kotatsu.core.db.MangaDatabase import org.koitharu.kotatsu.core.db.MangaDatabase
import org.koitharu.kotatsu.core.image.AvifImageDecoder import org.koitharu.kotatsu.core.image.AvifImageDecoder
import org.koitharu.kotatsu.core.image.CbzFetcher
import org.koitharu.kotatsu.core.image.MangaSourceHeaderInterceptor
import org.koitharu.kotatsu.core.network.MangaHttpClient import org.koitharu.kotatsu.core.network.MangaHttpClient
import org.koitharu.kotatsu.core.network.imageproxy.ImageProxyInterceptor import org.koitharu.kotatsu.core.network.imageproxy.ImageProxyInterceptor
import org.koitharu.kotatsu.core.os.AppShortcutManager import org.koitharu.kotatsu.core.os.AppShortcutManager
@ -44,7 +50,6 @@ import org.koitharu.kotatsu.core.util.ext.isLowRamDevice
import org.koitharu.kotatsu.details.ui.pager.pages.MangaPageFetcher import org.koitharu.kotatsu.details.ui.pager.pages.MangaPageFetcher
import org.koitharu.kotatsu.details.ui.pager.pages.MangaPageKeyer import org.koitharu.kotatsu.details.ui.pager.pages.MangaPageKeyer
import org.koitharu.kotatsu.local.data.CacheDir import org.koitharu.kotatsu.local.data.CacheDir
import org.koitharu.kotatsu.local.data.CbzFetcher
import org.koitharu.kotatsu.local.data.LocalStorageChanges import org.koitharu.kotatsu.local.data.LocalStorageChanges
import org.koitharu.kotatsu.local.domain.model.LocalManga import org.koitharu.kotatsu.local.domain.model.LocalManga
import org.koitharu.kotatsu.main.domain.CoverRestoreInterceptor import org.koitharu.kotatsu.main.domain.CoverRestoreInterceptor
@ -92,6 +97,7 @@ interface AppModule {
imageProxyInterceptor: ImageProxyInterceptor, imageProxyInterceptor: ImageProxyInterceptor,
pageFetcherFactory: MangaPageFetcher.Factory, pageFetcherFactory: MangaPageFetcher.Factory,
coverRestoreInterceptor: CoverRestoreInterceptor, coverRestoreInterceptor: CoverRestoreInterceptor,
networkStateProvider: Provider<NetworkState>,
): ImageLoader { ): ImageLoader {
val diskCacheFactory = { val diskCacheFactory = {
val rootDir = context.externalCacheDir ?: context.cacheDir val rootDir = context.externalCacheDir ?: context.cacheDir
@ -103,37 +109,38 @@ interface AppModule {
okHttpClientProvider.get().newBuilder().cache(null).build() okHttpClientProvider.get().newBuilder().cache(null).build()
} }
return ImageLoader.Builder(context) return ImageLoader.Builder(context)
.okHttpClient { okHttpClientLazy.value } .interceptorCoroutineContext(Dispatchers.Default)
.interceptorDispatcher(Dispatchers.Default)
.fetcherDispatcher(Dispatchers.Default)
.decoderDispatcher(Dispatchers.IO)
.transformationDispatcher(Dispatchers.Default)
.diskCache(diskCacheFactory) .diskCache(diskCacheFactory)
.respectCacheHeaders(false)
.networkObserverEnabled(false)
.logger(if (BuildConfig.DEBUG) DebugLogger() else null) .logger(if (BuildConfig.DEBUG) DebugLogger() else null)
.allowRgb565(context.isLowRamDevice()) .allowRgb565(context.isLowRamDevice())
.eventListener(CaptchaNotifier(context)) .eventListener(CaptchaNotifier(context))
.components( .components {
ComponentRegistry.Builder() add(
.add(SvgDecoder.Factory()) OkHttpNetworkFetcherFactory(
.add(CbzFetcher.Factory()) callFactory = okHttpClientLazy::value,
.add(AvifImageDecoder.Factory()) connectivityChecker = { networkStateProvider.get() },
.add(FaviconFetcher.Factory(context, okHttpClientLazy, mangaRepositoryFactory)) ),
.add(MangaPageKeyer()) )
.add(pageFetcherFactory) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
.add(imageProxyInterceptor) add(AnimatedImageDecoder.Factory())
.add(coverRestoreInterceptor) } else {
.build(), add(GifDecoder.Factory())
).build() }
add(CbzFetcher.Factory())
add(AvifImageDecoder.Factory())
add(FaviconFetcher.Factory(mangaRepositoryFactory))
add(MangaPageKeyer())
add(pageFetcherFactory)
add(imageProxyInterceptor)
add(coverRestoreInterceptor)
add(MangaSourceHeaderInterceptor())
}.build()
} }
@Provides @Provides
fun provideSearchSuggestions( fun provideSearchSuggestions(
@ApplicationContext context: Context, @ApplicationContext context: Context,
): SearchRecentSuggestions { ): SearchRecentSuggestions = MangaSuggestionsProvider.createSuggestions(context)
return MangaSuggestionsProvider.createSuggestions(context)
}
@Provides @Provides
@ElementsIntoSet @ElementsIntoSet

@ -1,24 +1,25 @@
package org.koitharu.kotatsu.core.image package org.koitharu.kotatsu.core.image
import android.graphics.Bitmap import android.graphics.Bitmap
import android.graphics.BitmapFactory import coil3.ImageLoader
import androidx.core.graphics.drawable.toDrawable import coil3.asImage
import coil.ImageLoader import coil3.decode.DecodeResult
import coil.decode.DecodeResult import coil3.decode.Decoder
import coil.decode.Decoder import coil3.decode.ImageSource
import coil.decode.ImageSource import coil3.fetch.SourceFetchResult
import coil.fetch.SourceResult import coil3.request.Options
import coil.request.Options
import com.davemorrissey.labs.subscaleview.decoder.ImageDecodeException import com.davemorrissey.labs.subscaleview.decoder.ImageDecodeException
import kotlinx.coroutines.sync.Semaphore import kotlinx.coroutines.runInterruptible
import org.aomedia.avif.android.AvifDecoder import org.aomedia.avif.android.AvifDecoder
import org.aomedia.avif.android.AvifDecoder.Info import org.aomedia.avif.android.AvifDecoder.Info
import org.koitharu.kotatsu.core.util.ext.toByteBuffer import org.koitharu.kotatsu.core.util.ext.toByteBuffer
class AvifImageDecoder(source: ImageSource, options: Options, parallelismLock: Semaphore) : class AvifImageDecoder(
BaseCoilDecoder(source, options, parallelismLock) { private val source: ImageSource,
private val options: Options,
) : Decoder {
override fun BitmapFactory.Options.decode(): DecodeResult { override suspend fun decode(): DecodeResult = runInterruptible {
val bytes = source.source().use { val bytes = source.source().use {
it.inputStream().toByteBuffer() it.inputStream().toByteBuffer()
} }
@ -36,22 +37,20 @@ class AvifImageDecoder(source: ImageSource, options: Options, parallelismLock: S
bitmap.recycle() bitmap.recycle()
throw ImageDecodeException(null, "avif") throw ImageDecodeException(null, "avif")
} }
return DecodeResult( DecodeResult(
drawable = bitmap.toDrawable(options.context.resources), image = bitmap.asImage(),
isSampled = false, isSampled = false,
) )
} }
class Factory : Decoder.Factory { class Factory : Decoder.Factory {
private val parallelismLock = Semaphore(DEFAULT_PARALLELISM)
override fun create( override fun create(
result: SourceResult, result: SourceFetchResult,
options: Options, options: Options,
imageLoader: ImageLoader imageLoader: ImageLoader
): Decoder? = if (isApplicable(result)) { ): Decoder? = if (isApplicable(result)) {
AvifImageDecoder(result.source, options, parallelismLock) AvifImageDecoder(result.source, options)
} else { } else {
null null
} }
@ -60,7 +59,7 @@ class AvifImageDecoder(source: ImageSource, options: Options, parallelismLock: S
override fun hashCode() = javaClass.hashCode() override fun hashCode() = javaClass.hashCode()
private fun isApplicable(result: SourceResult): Boolean { private fun isApplicable(result: SourceFetchResult): Boolean {
return result.mimeType == "image/avif" return result.mimeType == "image/avif"
} }
} }

@ -1,50 +0,0 @@
package org.koitharu.kotatsu.core.image
import android.graphics.BitmapFactory
import coil.decode.DecodeResult
import coil.decode.Decoder
import coil.decode.ImageSource
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 kotlinx.coroutines.runInterruptible
import kotlinx.coroutines.sync.Semaphore
import kotlinx.coroutines.sync.withPermit
import org.jetbrains.annotations.Blocking
abstract class BaseCoilDecoder(
protected val source: ImageSource,
protected val options: Options,
private val parallelismLock: Semaphore,
) : Decoder {
final override suspend fun decode(): DecodeResult = parallelismLock.withPermit {
runInterruptible { BitmapFactory.Options().decode() }
}
@Blocking
protected abstract fun BitmapFactory.Options.decode(): DecodeResult
protected companion object {
const val DEFAULT_PARALLELISM = 4
inline fun Size.widthPx(scale: Scale, original: () -> Int): Int {
return if (isOriginal) original() else width.toPx(scale)
}
inline fun Size.heightPx(scale: Scale, original: () -> Int): Int {
return if (isOriginal) original() else height.toPx(scale)
}
fun Dimension.toPx(scale: Scale) = pxOrElse {
when (scale) {
Scale.FILL -> Int.MIN_VALUE
Scale.FIT -> Int.MAX_VALUE
}
}
}
}

@ -0,0 +1,48 @@
package org.koitharu.kotatsu.core.image
import android.net.Uri
import android.webkit.MimeTypeMap
import coil3.ImageLoader
import coil3.decode.DataSource
import coil3.decode.ImageSource
import coil3.fetch.Fetcher
import coil3.fetch.SourceFetchResult
import coil3.request.Options
import coil3.toAndroidUri
import kotlinx.coroutines.runInterruptible
import okio.Path.Companion.toPath
import okio.openZip
import org.koitharu.kotatsu.core.util.ext.isZipUri
import coil3.Uri as CoilUri
class CbzFetcher(
private val uri: Uri,
private val options: Options,
) : Fetcher {
override suspend fun fetch() = runInterruptible {
val filePath = uri.schemeSpecificPart.toPath()
val entryName = requireNotNull(uri.fragment)
SourceFetchResult(
source = ImageSource(entryName.toPath(), options.fileSystem.openZip(filePath)),
mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(entryName.substringAfterLast('.', "")),
dataSource = DataSource.DISK,
)
}
class Factory : Fetcher.Factory<CoilUri> {
override fun create(
data: CoilUri,
options: Options,
imageLoader: ImageLoader
): Fetcher? {
val androidUri = data.toAndroidUri()
return if (androidUri.isZipUri()) {
CbzFetcher(androidUri, options)
} else {
null
}
}
}
}

@ -0,0 +1,23 @@
package org.koitharu.kotatsu.core.image
import coil3.intercept.Interceptor
import coil3.network.httpHeaders
import coil3.request.ImageResult
import org.koitharu.kotatsu.core.network.CommonHeaders
import org.koitharu.kotatsu.core.util.ext.mangaSourceKey
import org.koitharu.kotatsu.parsers.model.MangaParserSource
class MangaSourceHeaderInterceptor : Interceptor {
override suspend fun intercept(chain: Interceptor.Chain): ImageResult {
val mangaSource = chain.request.extras[mangaSourceKey] as? MangaParserSource ?: return chain.proceed()
val request = chain.request
val newHeaders = request.httpHeaders.newBuilder()
.set(CommonHeaders.MANGA_SOURCE, mangaSource.name)
.build()
val newRequest = request.newBuilder()
.httpHeaders(newHeaders)
.build()
return chain.withRequest(newRequest).proceed()
}
}

@ -5,22 +5,35 @@ import android.graphics.BitmapFactory
import android.graphics.BitmapRegionDecoder import android.graphics.BitmapRegionDecoder
import android.graphics.Rect import android.graphics.Rect
import android.os.Build import android.os.Build
import androidx.core.graphics.drawable.toDrawable import coil3.Extras
import coil.ImageLoader import coil3.ImageLoader
import coil.decode.DecodeResult import coil3.asImage
import coil.decode.DecodeUtils import coil3.decode.DecodeResult
import coil.decode.Decoder import coil3.decode.DecodeUtils
import coil.decode.ImageSource import coil3.decode.Decoder
import coil.fetch.SourceResult import coil3.decode.ImageSource
import coil.request.Options import coil3.fetch.SourceFetchResult
import kotlinx.coroutines.sync.Semaphore import coil3.getExtra
import coil3.request.Options
import coil3.request.allowRgb565
import coil3.request.bitmapConfig
import coil3.request.colorSpace
import coil3.request.premultipliedAlpha
import coil3.size.Dimension
import coil3.size.Precision
import coil3.size.Scale
import coil3.size.Size
import coil3.size.isOriginal
import coil3.size.pxOrElse
import kotlinx.coroutines.runInterruptible
import kotlin.math.roundToInt import kotlin.math.roundToInt
class RegionBitmapDecoder( class RegionBitmapDecoder(
source: ImageSource, options: Options, parallelismLock: Semaphore private val source: ImageSource,
) : BaseCoilDecoder(source, options, parallelismLock) { private val options: Options,
) : Decoder {
override fun BitmapFactory.Options.decode(): DecodeResult { override suspend fun decode(): DecodeResult = runInterruptible {
val regionDecoder = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { val regionDecoder = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
BitmapRegionDecoder.newInstance(source.source().inputStream()) BitmapRegionDecoder.newInstance(source.source().inputStream())
} else { } else {
@ -28,13 +41,14 @@ class RegionBitmapDecoder(
BitmapRegionDecoder.newInstance(source.source().inputStream(), false) BitmapRegionDecoder.newInstance(source.source().inputStream(), false)
} }
checkNotNull(regionDecoder) checkNotNull(regionDecoder)
val bitmapOptions = BitmapFactory.Options()
try { try {
val rect = configureScale(regionDecoder.width, regionDecoder.height) val rect = bitmapOptions.configureScale(regionDecoder.width, regionDecoder.height)
configureConfig() bitmapOptions.configureConfig()
val bitmap = regionDecoder.decodeRegion(rect, this) val bitmap = regionDecoder.decodeRegion(rect, bitmapOptions)
bitmap.density = options.context.resources.displayMetrics.densityDpi bitmap.density = options.context.resources.displayMetrics.densityDpi
return DecodeResult( DecodeResult(
drawable = bitmap.toDrawable(options.context.resources), image = bitmap.asImage(),
isSampled = true, isSampled = true,
) )
} finally { } finally {
@ -55,7 +69,7 @@ class RegionBitmapDecoder(
} else { } else {
Rect(0, 0, (srcHeight / dstRatio).toInt().coerceAtLeast(1), srcHeight) Rect(0, 0, (srcHeight / dstRatio).toInt().coerceAtLeast(1), srcHeight)
} }
val scroll = options.parameters.value(PARAM_SCROLL) ?: SCROLL_UNDEFINED val scroll = options.getExtra(regionScrollKey)
if (scroll == SCROLL_UNDEFINED) { if (scroll == SCROLL_UNDEFINED) {
rect.offsetTo( rect.offsetTo(
(srcWidth - rect.width()) / 2, (srcWidth - rect.width()) / 2,
@ -87,7 +101,7 @@ class RegionBitmapDecoder(
) )
// Only upscale the image if the options require an exact size. // Only upscale the image if the options require an exact size.
if (options.allowInexactSize) { if (options.precision == Precision.INEXACT) {
scale = scale.coerceAtMost(1.0) scale = scale.coerceAtMost(1.0)
} }
@ -107,7 +121,7 @@ class RegionBitmapDecoder(
} }
private fun BitmapFactory.Options.configureConfig() { private fun BitmapFactory.Options.configureConfig() {
var config = options.config var config = options.bitmapConfig
inMutable = false inMutable = false
@ -131,13 +145,11 @@ class RegionBitmapDecoder(
object Factory : Decoder.Factory { object Factory : Decoder.Factory {
private val parallelismLock = Semaphore(DEFAULT_PARALLELISM)
override fun create( override fun create(
result: SourceResult, result: SourceFetchResult,
options: Options, options: Options,
imageLoader: ImageLoader imageLoader: ImageLoader
): Decoder = RegionBitmapDecoder(result.source, options, parallelismLock) ): Decoder = RegionBitmapDecoder(result.source, options)
override fun equals(other: Any?) = other is Factory override fun equals(other: Any?) = other is Factory
@ -146,7 +158,22 @@ class RegionBitmapDecoder(
companion object { companion object {
const val PARAM_SCROLL = "scroll"
const val SCROLL_UNDEFINED = -1 const val SCROLL_UNDEFINED = -1
val regionScrollKey = Extras.Key(SCROLL_UNDEFINED)
private inline fun Size.widthPx(scale: Scale, original: () -> Int): Int {
return if (isOriginal) original() else width.toPx(scale)
}
private inline fun Size.heightPx(scale: Scale, original: () -> Int): Int {
return if (isOriginal) original() else height.toPx(scale)
}
private fun Dimension.toPx(scale: Scale) = pxOrElse {
when (scale) {
Scale.FILL -> Int.MIN_VALUE
Scale.FIT -> Int.MAX_VALUE
}
}
} }
} }

@ -16,6 +16,7 @@ object CommonHeaders {
const val CACHE_CONTROL = "Cache-Control" const val CACHE_CONTROL = "Cache-Control"
const val PROXY_AUTHORIZATION = "Proxy-Authorization" const val PROXY_AUTHORIZATION = "Proxy-Authorization"
const val RETRY_AFTER = "Retry-After" const val RETRY_AFTER = "Retry-After"
const val MANGA_SOURCE = "X-Manga-Source"
val CACHE_CONTROL_NO_STORE: CacheControl val CACHE_CONTROL_NO_STORE: CacheControl
get() = CacheControl.Builder().noStore().build() get() = CacheControl.Builder().noStore().build()

@ -9,11 +9,12 @@ import okhttp3.Request
import okhttp3.Response import okhttp3.Response
import okio.IOException import okio.IOException
import org.koitharu.kotatsu.BuildConfig import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.core.model.UnknownMangaSource import org.koitharu.kotatsu.core.model.MangaSource
import org.koitharu.kotatsu.core.parser.MangaLoaderContextImpl import org.koitharu.kotatsu.core.parser.MangaLoaderContextImpl
import org.koitharu.kotatsu.core.parser.MangaRepository import org.koitharu.kotatsu.core.parser.MangaRepository
import org.koitharu.kotatsu.core.parser.ParserMangaRepository import org.koitharu.kotatsu.core.parser.ParserMangaRepository
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
import org.koitharu.kotatsu.parsers.model.MangaParserSource
import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.util.mergeWith import org.koitharu.kotatsu.parsers.util.mergeWith
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
@ -30,15 +31,17 @@ class CommonHeadersInterceptor @Inject constructor(
override fun intercept(chain: Chain): Response { override fun intercept(chain: Chain): Response {
val request = chain.request() val request = chain.request()
val source = request.tag(MangaSource::class.java) val source = request.tag(MangaSource::class.java)
val repository = if (source == null || source == UnknownMangaSource) { ?: request.headers[CommonHeaders.MANGA_SOURCE]?.let { MangaSource(it) }
val repository = if (source is MangaParserSource) {
mangaRepositoryFactoryLazy.get().create(source) as? ParserMangaRepository
} else {
if (BuildConfig.DEBUG && source == null) { if (BuildConfig.DEBUG && source == null) {
Log.w("Http", "Request without source tag: ${request.url}") Log.w("Http", "Request without source tag: ${request.url}")
} }
null null
} else {
mangaRepositoryFactoryLazy.get().create(source) as? ParserMangaRepository
} }
val headersBuilder = request.headers.newBuilder() val headersBuilder = request.headers.newBuilder()
.removeAll(CommonHeaders.MANGA_SOURCE)
repository?.getRequestHeaders()?.let { repository?.getRequestHeaders()?.let {
headersBuilder.mergeWith(it, replaceExisting = false) headersBuilder.mergeWith(it, replaceExisting = false)
} }

@ -2,12 +2,12 @@ package org.koitharu.kotatsu.core.network.imageproxy
import android.util.Log import android.util.Log
import androidx.collection.ArraySet import androidx.collection.ArraySet
import coil.intercept.Interceptor import coil3.intercept.Interceptor
import coil.network.HttpException import coil3.network.HttpException
import coil.request.ErrorResult import coil3.request.ErrorResult
import coil.request.ImageRequest import coil3.request.ImageRequest
import coil.request.ImageResult import coil3.request.ImageResult
import coil.request.SuccessResult import coil3.request.SuccessResult
import okhttp3.HttpUrl import okhttp3.HttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
@ -35,14 +35,14 @@ abstract class BaseImageProxyInterceptor : ImageProxyInterceptor {
else -> null else -> null
} }
if (url == null || !url.isHttpOrHttps || url.host in blacklist) { if (url == null || !url.isHttpOrHttps || url.host in blacklist) {
return chain.proceed(request) return chain.proceed()
} }
val newRequest = onInterceptImageRequest(request, url) val newRequest = onInterceptImageRequest(request, url)
return when (val result = chain.proceed(newRequest)) { return when (val result = chain.withRequest(newRequest).proceed()) {
is SuccessResult -> result is SuccessResult -> result
is ErrorResult -> { is ErrorResult -> {
logDebug(result.throwable, newRequest.data) logDebug(result.throwable, newRequest.data)
chain.proceed(request).also { chain.proceed().also {
if (it is SuccessResult && result.throwable.isBlockedByServer()) { if (it is SuccessResult && result.throwable.isBlockedByServer()) {
blacklist.add(url.host) blacklist.add(url.host)
} }

@ -1,6 +1,6 @@
package org.koitharu.kotatsu.core.network.imageproxy package org.koitharu.kotatsu.core.network.imageproxy
import coil.intercept.Interceptor import coil3.intercept.Interceptor
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.Request import okhttp3.Request
import okhttp3.Response import okhttp3.Response

@ -1,7 +1,7 @@
package org.koitharu.kotatsu.core.network.imageproxy package org.koitharu.kotatsu.core.network.imageproxy
import coil.intercept.Interceptor import coil3.intercept.Interceptor
import coil.request.ImageResult import coil3.request.ImageResult
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.plus import kotlinx.coroutines.plus
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
@ -26,7 +26,7 @@ class RealImageProxyInterceptor @Inject constructor(
) )
override suspend fun intercept(chain: Interceptor.Chain): ImageResult { override suspend fun intercept(chain: Interceptor.Chain): ImageResult {
return delegate.value?.intercept(chain) ?: chain.proceed(chain.request) return delegate.value?.intercept(chain) ?: chain.proceed()
} }
override suspend fun interceptPageRequest(request: Request, okHttp: OkHttpClient): Response { override suspend fun interceptPageRequest(request: Request, okHttp: OkHttpClient): Response {

@ -1,8 +1,8 @@
package org.koitharu.kotatsu.core.network.imageproxy package org.koitharu.kotatsu.core.network.imageproxy
import coil.request.ImageRequest import coil3.request.ImageRequest
import coil.size.Dimension import coil3.size.Dimension
import coil.size.isOriginal import coil3.size.isOriginal
import okhttp3.HttpUrl import okhttp3.HttpUrl
import okhttp3.Request import okhttp3.Request

@ -1,6 +1,6 @@
package org.koitharu.kotatsu.core.network.imageproxy package org.koitharu.kotatsu.core.network.imageproxy
import coil.request.ImageRequest import coil3.request.ImageRequest
import okhttp3.HttpUrl import okhttp3.HttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrl import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Request import okhttp3.Request

@ -10,10 +10,11 @@ import androidx.core.content.pm.ShortcutManagerCompat
import androidx.core.graphics.drawable.IconCompat import androidx.core.graphics.drawable.IconCompat
import androidx.core.graphics.drawable.toBitmap import androidx.core.graphics.drawable.toBitmap
import androidx.room.InvalidationTracker import androidx.room.InvalidationTracker
import coil.ImageLoader import coil3.ImageLoader
import coil.request.ImageRequest import coil3.request.ImageRequest
import coil.size.Scale import coil3.request.transformations
import coil.size.Size import coil3.size.Scale
import coil3.size.Size
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
@ -27,9 +28,9 @@ import org.koitharu.kotatsu.core.parser.favicon.faviconUri
import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.ui.image.ThumbnailTransformation import org.koitharu.kotatsu.core.ui.image.ThumbnailTransformation
import org.koitharu.kotatsu.core.util.ext.getDrawableOrThrow import org.koitharu.kotatsu.core.util.ext.getDrawableOrThrow
import org.koitharu.kotatsu.core.util.ext.mangaSourceExtra
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
import org.koitharu.kotatsu.core.util.ext.processLifecycleScope import org.koitharu.kotatsu.core.util.ext.processLifecycleScope
import org.koitharu.kotatsu.core.util.ext.source
import org.koitharu.kotatsu.history.data.HistoryRepository import org.koitharu.kotatsu.history.data.HistoryRepository
import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.parsers.model.MangaSource
@ -138,7 +139,7 @@ class AppShortcutManager @Inject constructor(
ImageRequest.Builder(context) ImageRequest.Builder(context)
.data(manga.coverUrl) .data(manga.coverUrl)
.size(iconSize) .size(iconSize)
.source(manga.source) .mangaSourceExtra(manga.source)
.scale(Scale.FILL) .scale(Scale.FILL)
.transformations(ThumbnailTransformation()) .transformations(ThumbnailTransformation())
.build(), .build(),

@ -6,6 +6,7 @@ import android.net.Network
import android.net.NetworkCapabilities import android.net.NetworkCapabilities
import android.net.NetworkRequest import android.net.NetworkRequest
import android.os.Build import android.os.Build
import coil3.network.ConnectivityChecker
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.util.MediatorStateFlow import org.koitharu.kotatsu.core.util.MediatorStateFlow
@ -13,13 +14,17 @@ import org.koitharu.kotatsu.core.util.MediatorStateFlow
class NetworkState( class NetworkState(
private val connectivityManager: ConnectivityManager, private val connectivityManager: ConnectivityManager,
private val settings: AppSettings, private val settings: AppSettings,
) : MediatorStateFlow<Boolean>(connectivityManager.isOnline(settings)) { ) : MediatorStateFlow<Boolean>(connectivityManager.isOnline(settings)), ConnectivityChecker {
private val callback = NetworkCallbackImpl() private val callback = NetworkCallbackImpl()
override val value: Boolean override val value: Boolean
get() = connectivityManager.isOnline(settings) get() = connectivityManager.isOnline(settings)
override fun isOnline(): Boolean {
return connectivityManager.isOnline(settings)
}
@Synchronized @Synchronized
override fun onActive() { override fun onActive() {
invalidate() invalidate()

@ -2,7 +2,7 @@ package org.koitharu.kotatsu.core.parser
import android.util.Log import android.util.Log
import androidx.collection.MutableLongSet import androidx.collection.MutableLongSet
import coil.request.CachePolicy import coil3.request.CachePolicy
import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers

@ -1,7 +1,7 @@
package org.koitharu.kotatsu.core.parser package org.koitharu.kotatsu.core.parser
import android.net.Uri import android.net.Uri
import coil.request.CachePolicy import coil3.request.CachePolicy
import dagger.Reusable import dagger.Reusable
import org.koitharu.kotatsu.core.model.MangaSource import org.koitharu.kotatsu.core.model.MangaSource
import org.koitharu.kotatsu.core.model.UnknownMangaSource import org.koitharu.kotatsu.core.model.UnknownMangaSource

@ -1,6 +1,5 @@
package org.koitharu.kotatsu.core.parser.favicon package org.koitharu.kotatsu.core.parser.favicon
import android.content.Context
import android.graphics.Color import android.graphics.Color
import android.graphics.drawable.AdaptiveIconDrawable import android.graphics.drawable.AdaptiveIconDrawable
import android.graphics.drawable.ColorDrawable import android.graphics.drawable.ColorDrawable
@ -8,67 +7,43 @@ import android.graphics.drawable.Drawable
import android.graphics.drawable.LayerDrawable import android.graphics.drawable.LayerDrawable
import android.net.Uri import android.net.Uri
import android.os.Build import android.os.Build
import android.webkit.MimeTypeMap import coil3.ImageLoader
import coil.ImageLoader import coil3.asImage
import coil.decode.DataSource import coil3.decode.DataSource
import coil.decode.ImageSource import coil3.fetch.FetchResult
import coil.disk.DiskCache import coil3.fetch.Fetcher
import coil.fetch.DrawableResult import coil3.fetch.ImageFetchResult
import coil.fetch.FetchResult import coil3.request.Options
import coil.fetch.Fetcher import coil3.size.pxOrElse
import coil.fetch.SourceResult import coil3.toAndroidUri
import coil.network.HttpException
import coil.request.Options
import coil.size.Size
import coil.size.pxOrElse
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ensureActive import kotlinx.coroutines.ensureActive
import kotlinx.coroutines.runInterruptible import kotlinx.coroutines.runInterruptible
import okhttp3.OkHttpClient import okio.IOException
import okhttp3.Request
import okhttp3.Response
import okhttp3.ResponseBody
import okhttp3.internal.closeQuietly
import okio.Closeable
import okio.buffer
import org.koitharu.kotatsu.core.exceptions.CloudFlareProtectedException import org.koitharu.kotatsu.core.exceptions.CloudFlareProtectedException
import org.koitharu.kotatsu.core.model.MangaSource import org.koitharu.kotatsu.core.model.MangaSource
import org.koitharu.kotatsu.core.parser.EmptyMangaRepository import org.koitharu.kotatsu.core.parser.EmptyMangaRepository
import org.koitharu.kotatsu.core.parser.MangaRepository import org.koitharu.kotatsu.core.parser.MangaRepository
import org.koitharu.kotatsu.core.parser.ParserMangaRepository import org.koitharu.kotatsu.core.parser.ParserMangaRepository
import org.koitharu.kotatsu.core.parser.external.ExternalMangaRepository import org.koitharu.kotatsu.core.parser.external.ExternalMangaRepository
import org.koitharu.kotatsu.core.util.ext.writeAllCancellable import org.koitharu.kotatsu.core.util.ext.fetch
import org.koitharu.kotatsu.local.data.CacheDir
import org.koitharu.kotatsu.local.data.util.withExtraCloseable
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.util.await
import org.koitharu.kotatsu.parsers.util.requireBody
import java.net.HttpURLConnection
import kotlin.coroutines.coroutineContext import kotlin.coroutines.coroutineContext
import coil3.Uri as CoilUri
private const val FALLBACK_SIZE = 9999 // largest icon
class FaviconFetcher( class FaviconFetcher(
private val okHttpClient: OkHttpClient, private val uri: Uri,
private val diskCache: Lazy<DiskCache?>,
private val mangaSource: MangaSource,
private val options: Options, private val options: Options,
private val imageLoader: ImageLoader,
private val mangaRepositoryFactory: MangaRepository.Factory, private val mangaRepositoryFactory: MangaRepository.Factory,
) : Fetcher { ) : Fetcher {
private val diskCacheKey
get() = options.diskCacheKey ?: "${mangaSource.name}x${options.size.toCacheKey()}"
private val fileSystem
get() = checkNotNull(diskCache.value).fileSystem
override suspend fun fetch(): FetchResult { override suspend fun fetch(): FetchResult {
getCached(options)?.let { return it } val mangaSource = MangaSource(uri.schemeSpecificPart)
return when (val repo = mangaRepositoryFactory.create(mangaSource)) { return when (val repo = mangaRepositoryFactory.create(mangaSource)) {
is ParserMangaRepository -> fetchParserFavicon(repo) is ParserMangaRepository -> fetchParserFavicon(repo)
is ExternalMangaRepository -> fetchPluginIcon(repo) is ExternalMangaRepository -> fetchPluginIcon(repo)
is EmptyMangaRepository -> DrawableResult( is EmptyMangaRepository -> ImageFetchResult(
drawable = ColorDrawable(Color.WHITE), image = ColorDrawable(Color.WHITE).asImage(),
isSampled = false, isSampled = false,
dataSource = DataSource.MEMORY, dataSource = DataSource.MEMORY,
) )
@ -77,124 +52,65 @@ class FaviconFetcher(
} }
} }
private suspend fun fetchParserFavicon(repo: ParserMangaRepository): FetchResult { private suspend fun fetchParserFavicon(repository: ParserMangaRepository): FetchResult {
val sizePx = maxOf( val sizePx = maxOf(
options.size.width.pxOrElse { FALLBACK_SIZE }, options.size.width.pxOrElse { FALLBACK_SIZE },
options.size.height.pxOrElse { FALLBACK_SIZE }, options.size.height.pxOrElse { FALLBACK_SIZE },
) )
var favicons = repo.getFavicons() var favicons = repository.getFavicons()
var lastError: Exception? = null var lastError: Exception? = null
while (favicons.isNotEmpty()) { while (favicons.isNotEmpty()) {
coroutineContext.ensureActive() coroutineContext.ensureActive()
val icon = favicons.find(sizePx) ?: throwNSEE(lastError) val icon = favicons.find(sizePx) ?: throwNSEE(lastError)
val response = try { try {
loadIcon(icon.url, mangaSource) val result = imageLoader.fetch(icon.url, options)
if (result != null) {
return result
} else {
favicons -= icon
}
} catch (e: CloudFlareProtectedException) { } catch (e: CloudFlareProtectedException) {
throw e throw e
} catch (e: HttpException) { } catch (e: IOException) {
lastError = e lastError = e
favicons -= icon favicons -= icon
continue
} }
val responseBody = response.requireBody()
val source = writeToDiskCache(responseBody)?.toImageSource()?.also {
response.closeQuietly()
} ?: responseBody.toImageSource(response)
return SourceResult(
source = source,
mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(icon.type),
dataSource = response.toDataSource(),
)
} }
throwNSEE(lastError) throwNSEE(lastError)
} }
private suspend fun loadIcon(url: String, source: MangaSource): Response {
val request = Request.Builder()
.url(url)
.get()
.tag(MangaSource::class.java, source)
request.tag(MangaSource::class.java, source)
@Suppress("UNCHECKED_CAST")
options.tags.asMap().forEach { request.tag(it.key as Class<Any>, it.value) }
val response = okHttpClient.newCall(request.build()).await()
if (!response.isSuccessful && response.code != HttpURLConnection.HTTP_NOT_MODIFIED) {
response.closeQuietly()
throw HttpException(response)
}
return response
}
private suspend fun fetchPluginIcon(repository: ExternalMangaRepository): FetchResult { private suspend fun fetchPluginIcon(repository: ExternalMangaRepository): FetchResult {
val source = repository.source val source = repository.source
val pm = options.context.packageManager val pm = options.context.packageManager
val icon = runInterruptible(Dispatchers.IO) { val icon = runInterruptible {
val provider = pm.resolveContentProvider(source.authority, 0) val provider = pm.resolveContentProvider(source.authority, 0)
provider?.loadIcon(pm) ?: pm.getApplicationIcon(source.packageName) provider?.loadIcon(pm) ?: pm.getApplicationIcon(source.packageName)
} }
return DrawableResult( return ImageFetchResult(
drawable = icon.nonAdaptive(), image = icon.nonAdaptive().asImage(),
isSampled = false, isSampled = false,
dataSource = DataSource.DISK, dataSource = DataSource.DISK,
) )
} }
private fun getCached(options: Options): SourceResult? { class Factory(
if (!options.diskCachePolicy.readEnabled) { private val mangaRepositoryFactory: MangaRepository.Factory,
return null ) : Fetcher.Factory<CoilUri> {
}
val snapshot = diskCache.value?.openSnapshot(diskCacheKey) ?: return null override fun create(
return SourceResult( data: CoilUri,
source = snapshot.toImageSource(), options: Options,
mimeType = null, imageLoader: ImageLoader
dataSource = DataSource.DISK, ): Fetcher? = if (data.scheme == URI_SCHEME_FAVICON) {
) FaviconFetcher(data.toAndroidUri(), options, imageLoader, mangaRepositoryFactory)
} } else {
null
private suspend fun writeToDiskCache(body: ResponseBody): DiskCache.Snapshot? {
if (!options.diskCachePolicy.writeEnabled || body.contentLength() == 0L) {
return null
}
val editor = diskCache.value?.openEditor(diskCacheKey) ?: return null
try {
fileSystem.write(editor.data) {
writeAllCancellable(body.source())
}
return editor.commitAndOpenSnapshot()
} catch (e: Throwable) {
try {
editor.abort()
} catch (abortingError: Throwable) {
e.addSuppressed(abortingError)
}
body.closeQuietly()
throw e
} finally {
body.closeQuietly()
}
}
private fun DiskCache.Snapshot.toImageSource(): ImageSource {
return ImageSource(data, fileSystem, diskCacheKey, this)
} }
private fun ResponseBody.toImageSource(response: Closeable): ImageSource {
return ImageSource(
source().withExtraCloseable(response).buffer(),
options.context,
FaviconMetadata(mangaSource),
)
} }
private fun Response.toDataSource(): DataSource { private companion object {
return if (networkResponse != null) DataSource.NETWORK else DataSource.DISK
}
private fun Size.toCacheKey() = buildString { const val FALLBACK_SIZE = 9999 // largest icon
append(width.toString())
append('x')
append(height.toString())
}
private fun throwNSEE(lastError: Exception?): Nothing { private fun throwNSEE(lastError: Exception?): Nothing {
if (lastError != null) { if (lastError != null) {
@ -211,29 +127,6 @@ class FaviconFetcher(
this this
} }
class Factory(
context: Context,
okHttpClientLazy: Lazy<OkHttpClient>,
private val mangaRepositoryFactory: MangaRepository.Factory,
) : Fetcher.Factory<Uri> {
private val okHttpClient by okHttpClientLazy
private val diskCache = lazy {
val rootDir = context.externalCacheDir ?: context.cacheDir
DiskCache.Builder()
.directory(rootDir.resolve(CacheDir.FAVICONS.dir))
.build()
} }
override fun create(data: Uri, options: Options, imageLoader: ImageLoader): Fetcher? {
return if (data.scheme == URI_SCHEME_FAVICON) {
val mangaSource = MangaSource(data.schemeSpecificPart)
FaviconFetcher(okHttpClient, diskCache, mangaSource, options, mangaRepositoryFactory)
} else {
null
}
}
}
class FaviconMetadata(val source: MangaSource) : ImageSource.Metadata()
} }

@ -1,7 +1,7 @@
package org.koitharu.kotatsu.core.ui.image package org.koitharu.kotatsu.core.ui.image
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
import coil.target.GenericViewTarget import coil3.target.GenericViewTarget
import com.google.android.material.chip.Chip import com.google.android.material.chip.Chip
class ChipIconTarget(override val view: Chip) : GenericViewTarget<Chip>() { class ChipIconTarget(override val view: Chip) : GenericViewTarget<Chip>() {

@ -4,10 +4,12 @@ import android.content.Context
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
import android.text.Html import android.text.Html
import androidx.annotation.WorkerThread import androidx.annotation.WorkerThread
import coil.ImageLoader import coil3.ImageLoader
import coil.executeBlocking import coil3.executeBlocking
import coil.request.ImageRequest import coil3.request.ImageRequest
import coil3.request.allowHardware
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import org.koitharu.kotatsu.core.util.ext.drawable
import javax.inject.Inject import javax.inject.Inject
class CoilImageGetter @Inject constructor( class CoilImageGetter @Inject constructor(

@ -4,9 +4,9 @@ import android.view.View
import android.view.View.OnLayoutChangeListener import android.view.View.OnLayoutChangeListener
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.ImageView import android.widget.ImageView
import coil.size.Dimension import coil3.size.Dimension
import coil.size.Size import coil3.size.Size
import coil.size.ViewSizeResolver import coil3.size.ViewSizeResolver
import kotlinx.coroutines.CancellableContinuation import kotlinx.coroutines.CancellableContinuation
import kotlinx.coroutines.suspendCancellableCoroutine import kotlinx.coroutines.suspendCancellableCoroutine
import kotlin.coroutines.resume import kotlin.coroutines.resume

@ -2,11 +2,11 @@ package org.koitharu.kotatsu.core.ui.image
import android.graphics.Bitmap import android.graphics.Bitmap
import android.media.ThumbnailUtils import android.media.ThumbnailUtils
import coil.size.Size import coil3.size.Size
import coil.size.pxOrElse import coil3.size.pxOrElse
import coil.transform.Transformation import coil3.transform.Transformation
class ThumbnailTransformation : Transformation { class ThumbnailTransformation : Transformation() {
override val cacheKey: String = javaClass.name override val cacheKey: String = javaClass.name
@ -17,8 +17,4 @@ class ThumbnailTransformation : Transformation {
size.height.pxOrElse { input.height }, size.height.pxOrElse { input.height },
) )
} }
override fun equals(other: Any?) = other is ThumbnailTransformation
override fun hashCode() = javaClass.hashCode()
} }

@ -2,13 +2,13 @@ package org.koitharu.kotatsu.core.ui.image
import android.graphics.Bitmap import android.graphics.Bitmap
import androidx.core.graphics.get import androidx.core.graphics.get
import coil.size.Size import coil3.size.Size
import coil.transform.Transformation import coil3.transform.Transformation
import org.koitharu.kotatsu.reader.domain.EdgeDetector.Companion.isColorTheSame import org.koitharu.kotatsu.reader.domain.EdgeDetector.Companion.isColorTheSame
class TrimTransformation( class TrimTransformation(
private val tolerance: Int = 20, private val tolerance: Int = 20,
) : Transformation { ) : Transformation() {
override val cacheKey: String = "${javaClass.name}-$tolerance" override val cacheKey: String = "${javaClass.name}-$tolerance"
@ -92,12 +92,4 @@ class TrimTransformation(
input input
} }
} }
override fun equals(other: Any?): Boolean {
return this === other || (other is TrimTransformation && other.tolerance == tolerance)
}
override fun hashCode(): Int {
return tolerance
}
} }

@ -8,10 +8,16 @@ import androidx.annotation.ColorRes
import androidx.annotation.DrawableRes import androidx.annotation.DrawableRes
import androidx.annotation.StringRes import androidx.annotation.StringRes
import androidx.core.view.children import androidx.core.view.children
import coil.ImageLoader import coil3.ImageLoader
import coil.request.Disposable import coil3.request.Disposable
import coil.request.ImageRequest import coil3.request.ImageRequest
import coil.transform.RoundedCornersTransformation import coil3.request.allowRgb565
import coil3.request.crossfade
import coil3.request.error
import coil3.request.fallback
import coil3.request.placeholder
import coil3.request.transformations
import coil3.transform.RoundedCornersTransformation
import com.google.android.material.chip.Chip import com.google.android.material.chip.Chip
import com.google.android.material.chip.ChipDrawable import com.google.android.material.chip.ChipDrawable
import com.google.android.material.chip.ChipGroup import com.google.android.material.chip.ChipGroup

@ -2,21 +2,37 @@ package org.koitharu.kotatsu.core.util.ext
import android.content.Context import android.content.Context
import android.graphics.drawable.ColorDrawable import android.graphics.drawable.ColorDrawable
import android.graphics.drawable.Drawable
import android.widget.ImageView import android.widget.ImageView
import androidx.core.graphics.ColorUtils import androidx.core.graphics.ColorUtils
import androidx.core.graphics.drawable.toBitmap
import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleOwner
import coil.ImageLoader import coil3.Extras
import coil.request.ErrorResult import coil3.ImageLoader
import coil.request.ImageRequest import coil3.asDrawable
import coil.request.ImageResult import coil3.fetch.FetchResult
import coil.request.SuccessResult import coil3.request.ErrorResult
import coil.util.CoilUtils import coil3.request.ImageRequest
import coil3.request.ImageResult
import coil3.request.Options
import coil3.request.SuccessResult
import coil3.request.bitmapConfig
import coil3.request.crossfade
import coil3.request.error
import coil3.request.fallback
import coil3.request.lifecycle
import coil3.request.placeholder
import coil3.request.target
import coil3.size.Scale
import coil3.size.ViewSizeResolver
import coil3.toBitmap
import coil3.util.CoilUtils
import com.google.android.material.progressindicator.BaseProgressIndicator import com.google.android.material.progressindicator.BaseProgressIndicator
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.bookmarks.domain.Bookmark
import org.koitharu.kotatsu.core.image.RegionBitmapDecoder import org.koitharu.kotatsu.core.image.RegionBitmapDecoder
import org.koitharu.kotatsu.core.ui.image.AnimatedPlaceholderDrawable import org.koitharu.kotatsu.core.ui.image.AnimatedPlaceholderDrawable
import org.koitharu.kotatsu.core.util.progress.ImageRequestIndicatorListener import org.koitharu.kotatsu.core.util.progress.ImageRequestIndicatorListener
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.parsers.model.MangaSource
import com.google.android.material.R as materialR import com.google.android.material.R as materialR
@ -32,6 +48,8 @@ fun ImageView.newImageRequest(lifecycleOwner: LifecycleOwner, data: Any?): Image
.data(data?.takeUnless { it == "" || it == 0 }) .data(data?.takeUnless { it == "" || it == 0 })
.lifecycle(lifecycleOwner) .lifecycle(lifecycleOwner)
.crossfade(context) .crossfade(context)
.size(ViewSizeResolver(this))
.scale(scaleType.toCoilScale())
.target(this) .target(this)
} }
@ -43,13 +61,16 @@ fun ImageView.disposeImageRequest() {
fun ImageRequest.Builder.enqueueWith(loader: ImageLoader) = loader.enqueue(build()) fun ImageRequest.Builder.enqueueWith(loader: ImageLoader) = loader.enqueue(build())
fun ImageResult.getDrawableOrThrow() = when (this) { fun ImageResult.getDrawableOrThrow() = when (this) {
is SuccessResult -> drawable is SuccessResult -> image.asDrawable(request.context.resources)
is ErrorResult -> throw throwable is ErrorResult -> throw throwable
} }
val ImageResult.drawable: Drawable?
get() = image?.asDrawable(request.context.resources)
fun ImageResult.toBitmapOrNull() = when (this) { fun ImageResult.toBitmapOrNull() = when (this) {
is SuccessResult -> try { is SuccessResult -> try {
drawable.toBitmap() image.toBitmap(image.width, image.height, request.bitmapConfig)
} catch (_: Throwable) { } catch (_: Throwable) {
null null
} }
@ -63,8 +84,10 @@ fun ImageRequest.Builder.indicator(indicators: List<BaseProgressIndicator<*>>):
fun ImageRequest.Builder.decodeRegion( fun ImageRequest.Builder.decodeRegion(
scroll: Int = RegionBitmapDecoder.SCROLL_UNDEFINED, scroll: Int = RegionBitmapDecoder.SCROLL_UNDEFINED,
): ImageRequest.Builder = decoderFactory(RegionBitmapDecoder.Factory) ): ImageRequest.Builder = apply {
.setParameter(RegionBitmapDecoder.PARAM_SCROLL, scroll) decoderFactory(RegionBitmapDecoder.Factory)
extras[RegionBitmapDecoder.regionScrollKey] = scroll
}
@Suppress("SpellCheckingInspection") @Suppress("SpellCheckingInspection")
fun ImageRequest.Builder.crossfade(context: Context): ImageRequest.Builder { fun ImageRequest.Builder.crossfade(context: Context): ImageRequest.Builder {
@ -72,8 +95,18 @@ fun ImageRequest.Builder.crossfade(context: Context): ImageRequest.Builder {
return crossfade(duration.toInt()) return crossfade(duration.toInt())
} }
fun ImageRequest.Builder.source(source: MangaSource?): ImageRequest.Builder { fun ImageRequest.Builder.mangaSourceExtra(source: MangaSource?): ImageRequest.Builder = apply {
return tag(MangaSource::class.java, source) extras[mangaSourceKey] = source
}
fun ImageRequest.Builder.mangaExtra(manga: Manga): ImageRequest.Builder = apply {
extras[mangaKey] = manga
mangaSourceExtra(manga.source)
}
fun ImageRequest.Builder.bookmarkExtra(bookmark: Bookmark): ImageRequest.Builder = apply {
extras[bookmarkKey] = bookmark
mangaSourceExtra(bookmark.manga.source)
} }
fun ImageRequest.Builder.defaultPlaceholders(context: Context): ImageRequest.Builder { fun ImageRequest.Builder.defaultPlaceholders(context: Context): ImageRequest.Builder {
@ -87,6 +120,12 @@ fun ImageRequest.Builder.defaultPlaceholders(context: Context): ImageRequest.Bui
.error(ColorDrawable(errorColor)) .error(ColorDrawable(errorColor))
} }
private fun ImageView.ScaleType.toCoilScale(): Scale = if (this == ImageView.ScaleType.CENTER_CROP) {
Scale.FILL
} else {
Scale.FIT
}
fun ImageRequest.Builder.addListener(listener: ImageRequest.Listener): ImageRequest.Builder { fun ImageRequest.Builder.addListener(listener: ImageRequest.Listener): ImageRequest.Builder {
val existing = build().listener val existing = build().listener
return listener( return listener(
@ -98,6 +137,12 @@ fun ImageRequest.Builder.addListener(listener: ImageRequest.Listener): ImageRequ
) )
} }
suspend fun ImageLoader.fetch(data: Any, options: Options): FetchResult? {
val mappedData = components.map(data, options)
val fetcher = components.newFetcher(mappedData, options, this)?.first
return fetcher?.fetch()
}
private class CompositeImageRequestListener( private class CompositeImageRequestListener(
private val delegates: Array<ImageRequest.Listener>, private val delegates: Array<ImageRequest.Listener>,
) : ImageRequest.Listener { ) : ImageRequest.Listener {
@ -113,3 +158,7 @@ private class CompositeImageRequestListener(
operator fun plus(other: ImageRequest.Listener) = CompositeImageRequestListener(delegates + other) operator fun plus(other: ImageRequest.Listener) = CompositeImageRequestListener(delegates + other)
} }
val mangaKey = Extras.Key<Manga?>(null)
val bookmarkKey = Extras.Key<Bookmark?>(null)
val mangaSourceKey = Extras.Key<MangaSource?>(null)

@ -61,7 +61,7 @@ fun File.getStorageName(context: Context): String = runCatching {
} }
}.getOrNull() ?: context.getString(R.string.other_storage) }.getOrNull() ?: context.getString(R.string.other_storage)
fun Uri.toFileOrNull() = if (scheme == URI_SCHEME_FILE) path?.let(::File) else null fun Uri.toFileOrNull() = if (isFileUri()) path?.let(::File) else null
suspend fun File.deleteAwait() = runInterruptible(Dispatchers.IO) { suspend fun File.deleteAwait() = runInterruptible(Dispatchers.IO) {
delete() || deleteRecursively() delete() || deleteRecursively()

@ -3,8 +3,9 @@ package org.koitharu.kotatsu.core.util.ext
import android.content.ActivityNotFoundException import android.content.ActivityNotFoundException
import android.content.res.Resources import android.content.res.Resources
import androidx.annotation.DrawableRes import androidx.annotation.DrawableRes
import coil.network.HttpException import coil3.network.HttpException
import com.davemorrissey.labs.subscaleview.decoder.ImageDecodeException import com.davemorrissey.labs.subscaleview.decoder.ImageDecodeException
import okhttp3.Response
import okio.FileNotFoundException import okio.FileNotFoundException
import okio.IOException import okio.IOException
import okio.ProtocolException import okio.ProtocolException
@ -121,7 +122,7 @@ fun Throwable.getCauseUrl(): String? = when (this) {
is CloudFlareBlockedException -> url is CloudFlareBlockedException -> url
is CloudFlareProtectedException -> url is CloudFlareProtectedException -> url
is HttpStatusException -> url is HttpStatusException -> url
is HttpException -> response.request.url.toString() is HttpException -> (response.delegate as? Response)?.request?.url?.toString()
else -> null else -> null
} }

@ -1,62 +1,25 @@
package org.koitharu.kotatsu.core.util.ext package org.koitharu.kotatsu.core.util.ext
import android.net.Uri import android.net.Uri
import androidx.core.net.toFile
import okhttp3.internal.closeQuietly
import okio.Source
import okio.source
import okio.use
import org.jetbrains.annotations.Blocking
import org.koitharu.kotatsu.local.data.util.withExtraCloseable
import java.io.File import java.io.File
import java.util.zip.ZipFile
const val URI_SCHEME_FILE = "file"
const val URI_SCHEME_ZIP = "file+zip" const val URI_SCHEME_ZIP = "file+zip"
private const val URI_SCHEME_FILE = "file"
@Blocking private const val URI_SCHEME_HTTP = "http"
fun Uri.exists(): Boolean = when (scheme) { private const val URI_SCHEME_HTTPS = "https"
URI_SCHEME_FILE -> toFile().exists() private const val URI_SCHEME_LEGACY_CBZ = "cbz"
URI_SCHEME_ZIP -> { private const val URI_SCHEME_LEGACY_ZIP = "zip"
val file = File(requireNotNull(schemeSpecificPart))
file.exists() && ZipFile(file).use { it.getEntry(fragment) != null } fun Uri.isZipUri() = scheme.let {
} it == URI_SCHEME_ZIP || it == URI_SCHEME_LEGACY_CBZ || it == URI_SCHEME_LEGACY_ZIP
else -> unsupportedUri(this)
} }
@Blocking fun Uri.isFileUri() = scheme == URI_SCHEME_FILE
fun Uri.isTargetNotEmpty(): Boolean = when (scheme) {
URI_SCHEME_FILE -> toFile().isNotEmpty()
URI_SCHEME_ZIP -> {
val file = File(requireNotNull(schemeSpecificPart))
file.exists() && ZipFile(file).use { (it.getEntry(fragment)?.size ?: 0L) != 0L }
}
else -> unsupportedUri(this) fun Uri.isNetworkUri() = scheme.let {
} it == URI_SCHEME_HTTP || it == URI_SCHEME_HTTPS
@Blocking
fun Uri.source(): Source = when (scheme) {
URI_SCHEME_FILE -> toFile().source()
URI_SCHEME_ZIP -> {
val zip = ZipFile(schemeSpecificPart)
try {
val entry = zip.getEntry(fragment)
zip.getInputStream(entry).source().withExtraCloseable(zip)
} catch (e: Throwable) {
zip.closeQuietly()
throw e
}
}
else -> unsupportedUri(this)
} }
fun File.toZipUri(entryName: String): Uri = Uri.parse("$URI_SCHEME_ZIP://$absolutePath#$entryName") fun File.toZipUri(entryName: String): Uri = Uri.parse("$URI_SCHEME_ZIP://$absolutePath#$entryName")
fun String.toUriOrNull() = if (isEmpty()) null else Uri.parse(this) fun String.toUriOrNull() = if (isEmpty()) null else Uri.parse(this)
private fun unsupportedUri(uri: Uri): Nothing {
throw IllegalArgumentException("Bad uri $uri: only schemes $URI_SCHEME_FILE and $URI_SCHEME_ZIP are supported")
}

@ -1,8 +1,8 @@
package org.koitharu.kotatsu.core.util.progress package org.koitharu.kotatsu.core.util.progress
import coil.request.ErrorResult import coil3.request.ErrorResult
import coil.request.ImageRequest import coil3.request.ImageRequest
import coil.request.SuccessResult import coil3.request.SuccessResult
import com.google.android.material.progressindicator.BaseProgressIndicator import com.google.android.material.progressindicator.BaseProgressIndicator
class ImageRequestIndicatorListener( class ImageRequestIndicatorListener(

@ -26,11 +26,20 @@ import androidx.core.view.isVisible
import androidx.core.view.updateLayoutParams import androidx.core.view.updateLayoutParams
import androidx.core.view.updatePadding import androidx.core.view.updatePadding
import androidx.swiperefreshlayout.widget.CircularProgressDrawable import androidx.swiperefreshlayout.widget.CircularProgressDrawable
import coil.ImageLoader import coil3.ImageLoader
import coil.request.ImageRequest import coil3.request.ImageRequest
import coil.request.SuccessResult import coil3.request.SuccessResult
import coil.transform.RoundedCornersTransformation import coil3.request.allowRgb565
import coil.util.CoilUtils import coil3.request.crossfade
import coil3.request.error
import coil3.request.fallback
import coil3.request.lifecycle
import coil3.request.placeholder
import coil3.request.target
import coil3.request.transformations
import coil3.size.Scale
import coil3.transform.RoundedCornersTransformation
import coil3.util.CoilUtils
import com.google.android.material.chip.Chip import com.google.android.material.chip.Chip
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
@ -65,18 +74,19 @@ import org.koitharu.kotatsu.core.ui.widgets.ChipsView
import org.koitharu.kotatsu.core.util.FileSize import org.koitharu.kotatsu.core.util.FileSize
import org.koitharu.kotatsu.core.util.ext.crossfade import org.koitharu.kotatsu.core.util.ext.crossfade
import org.koitharu.kotatsu.core.util.ext.defaultPlaceholders import org.koitharu.kotatsu.core.util.ext.defaultPlaceholders
import org.koitharu.kotatsu.core.util.ext.drawable
import org.koitharu.kotatsu.core.util.ext.enqueueWith import org.koitharu.kotatsu.core.util.ext.enqueueWith
import org.koitharu.kotatsu.core.util.ext.getThemeColor import org.koitharu.kotatsu.core.util.ext.getThemeColor
import org.koitharu.kotatsu.core.util.ext.ifNullOrEmpty import org.koitharu.kotatsu.core.util.ext.ifNullOrEmpty
import org.koitharu.kotatsu.core.util.ext.isTextTruncated import org.koitharu.kotatsu.core.util.ext.isTextTruncated
import org.koitharu.kotatsu.core.util.ext.joinToStringWithLimit import org.koitharu.kotatsu.core.util.ext.joinToStringWithLimit
import org.koitharu.kotatsu.core.util.ext.mangaSourceExtra
import org.koitharu.kotatsu.core.util.ext.observe import org.koitharu.kotatsu.core.util.ext.observe
import org.koitharu.kotatsu.core.util.ext.observeEvent import org.koitharu.kotatsu.core.util.ext.observeEvent
import org.koitharu.kotatsu.core.util.ext.parentView import org.koitharu.kotatsu.core.util.ext.parentView
import org.koitharu.kotatsu.core.util.ext.scaleUpActivityOptionsOf import org.koitharu.kotatsu.core.util.ext.scaleUpActivityOptionsOf
import org.koitharu.kotatsu.core.util.ext.setNavigationBarTransparentCompat import org.koitharu.kotatsu.core.util.ext.setNavigationBarTransparentCompat
import org.koitharu.kotatsu.core.util.ext.setOnContextClickListenerCompat import org.koitharu.kotatsu.core.util.ext.setOnContextClickListenerCompat
import org.koitharu.kotatsu.core.util.ext.source
import org.koitharu.kotatsu.core.util.ext.textAndVisible import org.koitharu.kotatsu.core.util.ext.textAndVisible
import org.koitharu.kotatsu.databinding.ActivityDetailsBinding import org.koitharu.kotatsu.databinding.ActivityDetailsBinding
import org.koitharu.kotatsu.details.data.MangaDetails import org.koitharu.kotatsu.details.data.MangaDetails
@ -485,7 +495,7 @@ class DetailsActivity :
.placeholder(R.drawable.ic_web) .placeholder(R.drawable.ic_web)
.fallback(R.drawable.ic_web) .fallback(R.drawable.ic_web)
.error(R.drawable.ic_web) .error(R.drawable.ic_web)
.source(manga.source) .mangaSourceExtra(manga.source)
.transformations(RoundedCornersTransformation(resources.getDimension(R.dimen.chip_icon_corner))) .transformations(RoundedCornersTransformation(resources.getDimension(R.dimen.chip_icon_corner)))
.allowRgb565(true) .allowRgb565(true)
.enqueueWith(coil) .enqueueWith(coil)
@ -621,8 +631,9 @@ class DetailsActivity :
val request = ImageRequest.Builder(this) val request = ImageRequest.Builder(this)
.target(viewBinding.imageViewCover) .target(viewBinding.imageViewCover)
.size(CoverSizeResolver(viewBinding.imageViewCover)) .size(CoverSizeResolver(viewBinding.imageViewCover))
.scale(Scale.FILL)
.data(imageUrl) .data(imageUrl)
.tag(manga.source) .mangaSourceExtra(manga.source)
.crossfade(this) .crossfade(this)
.lifecycle(this) .lifecycle(this)
.placeholderMemoryCacheKey(manga.coverUrl) .placeholderMemoryCacheKey(manga.coverUrl)

@ -11,7 +11,7 @@ import androidx.appcompat.view.ActionMode
import androidx.core.graphics.Insets import androidx.core.graphics.Insets
import androidx.fragment.app.viewModels import androidx.fragment.app.viewModels
import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.GridLayoutManager
import coil.ImageLoader import coil3.ImageLoader
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.bookmarks.domain.Bookmark import org.koitharu.kotatsu.bookmarks.domain.Bookmark

@ -1,60 +1,53 @@
package org.koitharu.kotatsu.details.ui.pager.pages package org.koitharu.kotatsu.details.ui.pager.pages
import android.content.Context
import android.webkit.MimeTypeMap import android.webkit.MimeTypeMap
import androidx.core.net.toFile
import androidx.core.net.toUri import androidx.core.net.toUri
import coil.ImageLoader import coil3.ImageLoader
import coil.decode.DataSource import coil3.decode.DataSource
import coil.decode.ImageSource import coil3.decode.ImageSource
import coil.fetch.FetchResult import coil3.fetch.FetchResult
import coil.fetch.Fetcher import coil3.fetch.Fetcher
import coil.fetch.SourceResult import coil3.fetch.SourceFetchResult
import coil.network.HttpException import coil3.network.HttpException
import coil.request.Options import coil3.network.NetworkHeaders
import dagger.hilt.android.qualifiers.ApplicationContext import coil3.network.NetworkResponse
import kotlinx.coroutines.Dispatchers import coil3.network.NetworkResponseBody
import kotlinx.coroutines.runInterruptible import coil3.request.Options
import okhttp3.Headers
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.internal.closeQuietly import okhttp3.Response
import okio.FileSystem
import okio.Path.Companion.toOkioPath import okio.Path.Companion.toOkioPath
import okio.buffer
import okio.source
import org.koitharu.kotatsu.core.network.MangaHttpClient import org.koitharu.kotatsu.core.network.MangaHttpClient
import org.koitharu.kotatsu.core.network.imageproxy.ImageProxyInterceptor import org.koitharu.kotatsu.core.network.imageproxy.ImageProxyInterceptor
import org.koitharu.kotatsu.core.parser.MangaRepository import org.koitharu.kotatsu.core.parser.MangaRepository
import org.koitharu.kotatsu.core.util.ext.fetch
import org.koitharu.kotatsu.core.util.ext.isNetworkUri
import org.koitharu.kotatsu.local.data.PagesCache import org.koitharu.kotatsu.local.data.PagesCache
import org.koitharu.kotatsu.local.data.isFileUri
import org.koitharu.kotatsu.local.data.isZipUri
import org.koitharu.kotatsu.local.data.util.withExtraCloseable
import org.koitharu.kotatsu.parsers.model.MangaPage import org.koitharu.kotatsu.parsers.model.MangaPage
import org.koitharu.kotatsu.parsers.util.mimeType import org.koitharu.kotatsu.parsers.util.mimeType
import org.koitharu.kotatsu.parsers.util.requireBody import org.koitharu.kotatsu.parsers.util.requireBody
import org.koitharu.kotatsu.reader.domain.PageLoader import org.koitharu.kotatsu.reader.domain.PageLoader
import java.util.zip.ZipFile
import javax.inject.Inject import javax.inject.Inject
class MangaPageFetcher( class MangaPageFetcher(
private val context: Context,
private val okHttpClient: OkHttpClient, private val okHttpClient: OkHttpClient,
private val pagesCache: PagesCache, private val pagesCache: PagesCache,
private val options: Options, private val options: Options,
private val page: MangaPage, private val page: MangaPage,
private val mangaRepositoryFactory: MangaRepository.Factory, private val mangaRepositoryFactory: MangaRepository.Factory,
private val imageProxyInterceptor: ImageProxyInterceptor, private val imageProxyInterceptor: ImageProxyInterceptor,
private val imageLoader: ImageLoader,
) : Fetcher { ) : Fetcher {
override suspend fun fetch(): FetchResult { override suspend fun fetch(): FetchResult? {
val repo = mangaRepositoryFactory.create(page.source) val repo = mangaRepositoryFactory.create(page.source)
val pageUrl = repo.getPageUrl(page) val pageUrl = repo.getPageUrl(page)
if (options.diskCachePolicy.readEnabled) { if (options.diskCachePolicy.readEnabled) {
pagesCache.get(pageUrl)?.let { file -> pagesCache.get(pageUrl)?.let { file ->
return SourceResult( return SourceFetchResult(
source = ImageSource( source = ImageSource(file.toOkioPath(), options.fileSystem),
file = file.toOkioPath(), mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(file.extension),
metadata = MangaPageMetadata(page),
),
mimeType = null,
dataSource = DataSource.DISK, dataSource = DataSource.DISK,
) )
} }
@ -62,67 +55,50 @@ class MangaPageFetcher(
return loadPage(pageUrl) return loadPage(pageUrl)
} }
private suspend fun loadPage(pageUrl: String): SourceResult { private suspend fun loadPage(pageUrl: String): FetchResult? = if (pageUrl.toUri().isNetworkUri()) {
val uri = pageUrl.toUri() fetchPage(pageUrl)
return when { } else {
uri.isZipUri() -> runInterruptible(Dispatchers.IO) { imageLoader.fetch(pageUrl, options)
val zip = ZipFile(uri.schemeSpecificPart)
try {
val entry = zip.getEntry(uri.fragment)
SourceResult(
source = ImageSource(
source = zip.getInputStream(entry).source().withExtraCloseable(zip).buffer(),
context = context,
metadata = MangaPageMetadata(page),
),
mimeType = MimeTypeMap.getSingleton()
.getMimeTypeFromExtension(entry.name.substringAfterLast('.', "")),
dataSource = DataSource.DISK,
)
} catch (e: Throwable) {
zip.closeQuietly()
throw e
}
}
uri.isFileUri() -> runInterruptible(Dispatchers.IO) {
val file = uri.toFile()
SourceResult(
source = ImageSource(
source = file.source().buffer(),
context = context,
metadata = MangaPageMetadata(page),
),
mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(file.extension),
dataSource = DataSource.DISK,
)
} }
else -> { private suspend fun fetchPage(pageUrl: String): FetchResult {
val request = PageLoader.createPageRequest(pageUrl, page.source) val request = PageLoader.createPageRequest(pageUrl, page.source)
imageProxyInterceptor.interceptPageRequest(request, okHttpClient).use { response -> return imageProxyInterceptor.interceptPageRequest(request, okHttpClient).use { response ->
if (!response.isSuccessful) { if (!response.isSuccessful) {
throw HttpException(response) throw HttpException(response.toNetworkResponse())
} }
val mimeType = response.mimeType val mimeType = response.mimeType
val file = response.requireBody().use { val file = response.requireBody().use {
pagesCache.put(pageUrl, it.source(), mimeType) pagesCache.put(pageUrl, it.source(), mimeType)
} }
SourceResult( SourceFetchResult(
source = ImageSource( source = ImageSource(file.toOkioPath(), FileSystem.SYSTEM),
file = file.toOkioPath(),
metadata = MangaPageMetadata(page),
),
mimeType = mimeType, mimeType = mimeType,
dataSource = DataSource.NETWORK, dataSource = DataSource.NETWORK,
) )
} }
} }
private fun Response.toNetworkResponse(): NetworkResponse {
return NetworkResponse(
code = code,
requestMillis = sentRequestAtMillis,
responseMillis = receivedResponseAtMillis,
headers = headers.toNetworkHeaders(),
body = body?.source()?.let(::NetworkResponseBody),
delegate = this,
)
} }
private fun Headers.toNetworkHeaders(): NetworkHeaders {
val headers = NetworkHeaders.Builder()
for ((key, values) in this) {
headers.add(key, values)
}
return headers.build()
} }
class Factory @Inject constructor( class Factory @Inject constructor(
@ApplicationContext private val context: Context,
@MangaHttpClient private val okHttpClient: OkHttpClient, @MangaHttpClient private val okHttpClient: OkHttpClient,
private val pagesCache: PagesCache, private val pagesCache: PagesCache,
private val mangaRepositoryFactory: MangaRepository.Factory, private val mangaRepositoryFactory: MangaRepository.Factory,
@ -134,11 +110,9 @@ class MangaPageFetcher(
pagesCache = pagesCache, pagesCache = pagesCache,
options = options, options = options,
page = data, page = data,
context = context,
mangaRepositoryFactory = mangaRepositoryFactory, mangaRepositoryFactory = mangaRepositoryFactory,
imageProxyInterceptor = imageProxyInterceptor, imageProxyInterceptor = imageProxyInterceptor,
imageLoader = imageLoader,
) )
} }
class MangaPageMetadata(val page: MangaPage) : ImageSource.Metadata()
} }

@ -1,7 +1,7 @@
package org.koitharu.kotatsu.details.ui.pager.pages package org.koitharu.kotatsu.details.ui.pager.pages
import coil.key.Keyer import coil3.key.Keyer
import coil.request.Options import coil3.request.Options
import org.koitharu.kotatsu.parsers.model.MangaPage import org.koitharu.kotatsu.parsers.model.MangaPage
class MangaPageKeyer : Keyer<MangaPage> { class MangaPageKeyer : Keyer<MangaPage> {

@ -1,9 +1,10 @@
package org.koitharu.kotatsu.details.ui.pager.pages package org.koitharu.kotatsu.details.ui.pager.pages
import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleOwner
import coil.ImageLoader import coil3.ImageLoader
import coil.size.Scale import coil3.request.allowRgb565
import coil.size.Size import coil3.size.Scale
import coil3.size.Size
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.ui.list.AdapterDelegateClickListenerAdapter import org.koitharu.kotatsu.core.ui.list.AdapterDelegateClickListenerAdapter
@ -11,9 +12,9 @@ import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.util.ext.decodeRegion import org.koitharu.kotatsu.core.util.ext.decodeRegion
import org.koitharu.kotatsu.core.util.ext.defaultPlaceholders import org.koitharu.kotatsu.core.util.ext.defaultPlaceholders
import org.koitharu.kotatsu.core.util.ext.enqueueWith import org.koitharu.kotatsu.core.util.ext.enqueueWith
import org.koitharu.kotatsu.core.util.ext.mangaSourceExtra
import org.koitharu.kotatsu.core.util.ext.newImageRequest import org.koitharu.kotatsu.core.util.ext.newImageRequest
import org.koitharu.kotatsu.core.util.ext.setTextColorAttr import org.koitharu.kotatsu.core.util.ext.setTextColorAttr
import org.koitharu.kotatsu.core.util.ext.source
import org.koitharu.kotatsu.databinding.ItemPageThumbBinding import org.koitharu.kotatsu.databinding.ItemPageThumbBinding
import org.koitharu.kotatsu.list.ui.model.ListModel import org.koitharu.kotatsu.list.ui.model.ListModel
import com.google.android.material.R as materialR import com.google.android.material.R as materialR
@ -42,13 +43,13 @@ fun pageThumbnailAD(
scale(Scale.FILL) scale(Scale.FILL)
allowRgb565(true) allowRgb565(true)
decodeRegion(0) decodeRegion(0)
source(item.page.source) mangaSourceExtra(item.page.source)
enqueueWith(coil) enqueueWith(coil)
} }
with(binding.textViewNumber) { with(binding.textViewNumber) {
setBackgroundResource(if (item.isCurrent) R.drawable.bg_badge_accent else R.drawable.bg_badge_empty) setBackgroundResource(if (item.isCurrent) R.drawable.bg_badge_accent else R.drawable.bg_badge_empty)
setTextColorAttr(if (item.isCurrent) materialR.attr.colorOnTertiary else android.R.attr.textColorPrimary) setTextColorAttr(if (item.isCurrent) materialR.attr.colorOnTertiary else android.R.attr.textColorPrimary)
text = (item.number).toString() text = item.number.toString()
} }
} }
} }

@ -2,7 +2,7 @@ package org.koitharu.kotatsu.details.ui.pager.pages
import android.content.Context import android.content.Context
import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleOwner
import coil.ImageLoader import coil3.ImageLoader
import org.koitharu.kotatsu.core.ui.BaseListAdapter import org.koitharu.kotatsu.core.ui.BaseListAdapter
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.ui.list.fastscroll.FastScroller import org.koitharu.kotatsu.core.ui.list.fastscroll.FastScroller

@ -10,7 +10,7 @@ import androidx.core.view.isVisible
import androidx.fragment.app.viewModels import androidx.fragment.app.viewModels
import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import coil.ImageLoader import coil3.ImageLoader
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine

@ -2,7 +2,7 @@ package org.koitharu.kotatsu.details.ui.scrobbling
import androidx.fragment.app.FragmentManager import androidx.fragment.app.FragmentManager
import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleOwner
import coil.ImageLoader import coil3.ImageLoader
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.util.ext.defaultPlaceholders import org.koitharu.kotatsu.core.util.ext.defaultPlaceholders

@ -14,7 +14,7 @@ import androidx.core.net.toUri
import androidx.core.text.method.LinkMovementMethodCompat import androidx.core.text.method.LinkMovementMethodCompat
import androidx.fragment.app.FragmentManager import androidx.fragment.app.FragmentManager
import androidx.fragment.app.activityViewModels import androidx.fragment.app.activityViewModels
import coil.ImageLoader import coil3.ImageLoader
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.ui.sheet.BaseAdaptiveSheet import org.koitharu.kotatsu.core.ui.sheet.BaseAdaptiveSheet

@ -2,7 +2,7 @@ package org.koitharu.kotatsu.details.ui.scrobbling
import androidx.fragment.app.FragmentManager import androidx.fragment.app.FragmentManager
import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleOwner
import coil.ImageLoader import coil3.ImageLoader
import org.koitharu.kotatsu.core.ui.BaseListAdapter import org.koitharu.kotatsu.core.ui.BaseListAdapter
import org.koitharu.kotatsu.list.ui.model.ListModel import org.koitharu.kotatsu.list.ui.model.ListModel

@ -7,9 +7,11 @@ import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.work.WorkInfo import androidx.work.WorkInfo
import coil.ImageLoader import coil3.ImageLoader
import coil.request.SuccessResult import coil3.request.SuccessResult
import coil.util.CoilUtils import coil3.request.allowRgb565
import coil3.request.transformations
import coil3.util.CoilUtils
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
import kotlinx.coroutines.CoroutineStart import kotlinx.coroutines.CoroutineStart
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
@ -19,8 +21,8 @@ import org.koitharu.kotatsu.core.ui.BaseListAdapter
import org.koitharu.kotatsu.core.ui.image.TrimTransformation import org.koitharu.kotatsu.core.ui.image.TrimTransformation
import org.koitharu.kotatsu.core.util.ext.defaultPlaceholders import org.koitharu.kotatsu.core.util.ext.defaultPlaceholders
import org.koitharu.kotatsu.core.util.ext.enqueueWith import org.koitharu.kotatsu.core.util.ext.enqueueWith
import org.koitharu.kotatsu.core.util.ext.mangaSourceExtra
import org.koitharu.kotatsu.core.util.ext.newImageRequest import org.koitharu.kotatsu.core.util.ext.newImageRequest
import org.koitharu.kotatsu.core.util.ext.source
import org.koitharu.kotatsu.core.util.ext.textAndVisible import org.koitharu.kotatsu.core.util.ext.textAndVisible
import org.koitharu.kotatsu.databinding.ItemDownloadBinding import org.koitharu.kotatsu.databinding.ItemDownloadBinding
import org.koitharu.kotatsu.download.ui.list.chapters.DownloadChapter import org.koitharu.kotatsu.download.ui.list.chapters.DownloadChapter
@ -92,7 +94,7 @@ fun downloadItemAD(
allowRgb565(true) allowRgb565(true)
transformations(TrimTransformation()) transformations(TrimTransformation())
memoryCacheKey(item.coverCacheKey) memoryCacheKey(item.coverCacheKey)
source(item.manga?.source) mangaSourceExtra(item.manga?.source)
enqueueWith(coil) enqueueWith(coil)
} }
} }

@ -7,7 +7,7 @@ import androidx.core.text.bold
import androidx.core.text.buildSpannedString import androidx.core.text.buildSpannedString
import androidx.core.text.color import androidx.core.text.color
import androidx.work.WorkInfo import androidx.work.WorkInfo
import coil.memory.MemoryCache import coil3.memory.MemoryCache
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import org.koitharu.kotatsu.core.util.ext.getThemeColor import org.koitharu.kotatsu.core.util.ext.getThemeColor
import org.koitharu.kotatsu.download.ui.list.chapters.DownloadChapter import org.koitharu.kotatsu.download.ui.list.chapters.DownloadChapter

@ -9,7 +9,7 @@ import androidx.activity.viewModels
import androidx.appcompat.view.ActionMode import androidx.appcompat.view.ActionMode
import androidx.core.graphics.Insets import androidx.core.graphics.Insets
import androidx.core.view.updatePadding import androidx.core.view.updatePadding
import coil.ImageLoader import coil3.ImageLoader
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.ui.BaseActivity import org.koitharu.kotatsu.core.ui.BaseActivity

@ -1,7 +1,7 @@
package org.koitharu.kotatsu.download.ui.list package org.koitharu.kotatsu.download.ui.list
import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleOwner
import coil.ImageLoader import coil3.ImageLoader
import org.koitharu.kotatsu.core.ui.BaseListAdapter import org.koitharu.kotatsu.core.ui.BaseListAdapter
import org.koitharu.kotatsu.list.ui.adapter.ListItemType import org.koitharu.kotatsu.list.ui.adapter.ListItemType
import org.koitharu.kotatsu.list.ui.adapter.emptyStateListAD import org.koitharu.kotatsu.list.ui.adapter.emptyStateListAD

@ -12,9 +12,10 @@ import androidx.core.app.NotificationManagerCompat
import androidx.core.app.PendingIntentCompat import androidx.core.app.PendingIntentCompat
import androidx.core.graphics.drawable.toBitmap import androidx.core.graphics.drawable.toBitmap
import androidx.work.WorkManager import androidx.work.WorkManager
import coil.ImageLoader import coil3.ImageLoader
import coil.request.ImageRequest import coil3.request.ImageRequest
import coil.size.Scale import coil3.request.allowHardware
import coil3.size.Scale
import dagger.assisted.Assisted import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject import dagger.assisted.AssistedInject
@ -26,6 +27,7 @@ import org.koitharu.kotatsu.core.ErrorReporterReceiver
import org.koitharu.kotatsu.core.model.LocalMangaSource import org.koitharu.kotatsu.core.model.LocalMangaSource
import org.koitharu.kotatsu.core.util.ext.getDrawableOrThrow import org.koitharu.kotatsu.core.util.ext.getDrawableOrThrow
import org.koitharu.kotatsu.core.util.ext.isReportable import org.koitharu.kotatsu.core.util.ext.isReportable
import org.koitharu.kotatsu.core.util.ext.mangaSourceExtra
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
import org.koitharu.kotatsu.details.ui.DetailsActivity import org.koitharu.kotatsu.details.ui.DetailsActivity
import org.koitharu.kotatsu.download.domain.DownloadState import org.koitharu.kotatsu.download.domain.DownloadState
@ -279,7 +281,7 @@ class DownloadNotificationFactory @AssistedInject constructor(
ImageRequest.Builder(context) ImageRequest.Builder(context)
.data(manga.coverUrl) .data(manga.coverUrl)
.allowHardware(false) .allowHardware(false)
.tag(manga.source) .mangaSourceExtra(manga.source)
.size(coverWidth, coverHeight) .size(coverWidth, coverHeight)
.scale(Scale.FILL) .scale(Scale.FILL)
.build(), .build(),

@ -18,7 +18,7 @@ import androidx.fragment.app.viewModels
import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import coil.ImageLoader import coil3.ImageLoader
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.bookmarks.ui.AllBookmarksActivity import org.koitharu.kotatsu.bookmarks.ui.AllBookmarksActivity

@ -1,7 +1,7 @@
package org.koitharu.kotatsu.explore.ui.adapter package org.koitharu.kotatsu.explore.ui.adapter
import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleOwner
import coil.ImageLoader import coil3.ImageLoader
import org.koitharu.kotatsu.core.ui.BaseListAdapter import org.koitharu.kotatsu.core.ui.BaseListAdapter
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.explore.ui.model.MangaSourceItem import org.koitharu.kotatsu.explore.ui.model.MangaSourceItem

@ -3,7 +3,12 @@ package org.koitharu.kotatsu.explore.ui.adapter
import android.view.View import android.view.View
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleOwner
import coil.ImageLoader import coil3.ImageLoader
import coil3.request.allowRgb565
import coil3.request.error
import coil3.request.fallback
import coil3.request.placeholder
import coil3.request.transformations
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.model.getSummary import org.koitharu.kotatsu.core.model.getSummary
@ -18,10 +23,10 @@ import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.util.ext.defaultPlaceholders import org.koitharu.kotatsu.core.util.ext.defaultPlaceholders
import org.koitharu.kotatsu.core.util.ext.drawableStart import org.koitharu.kotatsu.core.util.ext.drawableStart
import org.koitharu.kotatsu.core.util.ext.enqueueWith import org.koitharu.kotatsu.core.util.ext.enqueueWith
import org.koitharu.kotatsu.core.util.ext.mangaSourceExtra
import org.koitharu.kotatsu.core.util.ext.newImageRequest import org.koitharu.kotatsu.core.util.ext.newImageRequest
import org.koitharu.kotatsu.core.util.ext.recyclerView import org.koitharu.kotatsu.core.util.ext.recyclerView
import org.koitharu.kotatsu.core.util.ext.setProgressIcon import org.koitharu.kotatsu.core.util.ext.setProgressIcon
import org.koitharu.kotatsu.core.util.ext.source
import org.koitharu.kotatsu.core.util.ext.textAndVisible import org.koitharu.kotatsu.core.util.ext.textAndVisible
import org.koitharu.kotatsu.databinding.ItemExploreButtonsBinding import org.koitharu.kotatsu.databinding.ItemExploreButtonsBinding
import org.koitharu.kotatsu.databinding.ItemExploreSourceGridBinding import org.koitharu.kotatsu.databinding.ItemExploreSourceGridBinding
@ -94,7 +99,7 @@ fun recommendationMangaItemAD(
defaultPlaceholders(context) defaultPlaceholders(context)
allowRgb565(true) allowRgb565(true)
transformations(TrimTransformation()) transformations(TrimTransformation())
source(item.manga.source) mangaSourceExtra(item.manga.source)
enqueueWith(coil) enqueueWith(coil)
} }
} }
@ -128,7 +133,7 @@ fun exploreSourceListItemAD(
fallback(fallbackIcon) fallback(fallbackIcon)
placeholder(AnimatedFaviconDrawable(context, R.style.FaviconDrawable_Small, item.source.name)) placeholder(AnimatedFaviconDrawable(context, R.style.FaviconDrawable_Small, item.source.name))
error(fallbackIcon) error(fallbackIcon)
source(item.source) mangaSourceExtra(item.source)
enqueueWith(coil) enqueueWith(coil)
} }
} }
@ -160,7 +165,7 @@ fun exploreSourceGridItemAD(
fallback(fallbackIcon) fallback(fallbackIcon)
placeholder(AnimatedFaviconDrawable(context, R.style.FaviconDrawable_Large, item.source.name)) placeholder(AnimatedFaviconDrawable(context, R.style.FaviconDrawable_Large, item.source.name))
error(fallbackIcon) error(fallbackIcon)
source(item.source) mangaSourceExtra(item.source)
enqueueWith(coil) enqueueWith(coil)
} }
} }

@ -10,7 +10,7 @@ import androidx.core.view.updateLayoutParams
import androidx.core.view.updatePadding import androidx.core.view.updatePadding
import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import coil.ImageLoader import coil3.ImageLoader
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.exceptions.resolve.SnackbarErrorObserver import org.koitharu.kotatsu.core.exceptions.resolve.SnackbarErrorObserver

@ -1,7 +1,7 @@
package org.koitharu.kotatsu.favourites.ui.categories.adapter package org.koitharu.kotatsu.favourites.ui.categories.adapter
import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleOwner
import coil.ImageLoader import coil3.ImageLoader
import org.koitharu.kotatsu.core.ui.ReorderableListAdapter import org.koitharu.kotatsu.core.ui.ReorderableListAdapter
import org.koitharu.kotatsu.favourites.ui.categories.FavouriteCategoriesListListener import org.koitharu.kotatsu.favourites.ui.categories.FavouriteCategoriesListListener
import org.koitharu.kotatsu.list.ui.adapter.ListItemType import org.koitharu.kotatsu.list.ui.adapter.ListItemType

@ -14,14 +14,19 @@ import androidx.core.view.isGone
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.core.widget.ImageViewCompat import androidx.core.widget.ImageViewCompat
import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleOwner
import coil.ImageLoader import coil3.ImageLoader
import coil3.request.allowRgb565
import coil3.request.crossfade
import coil3.request.error
import coil3.request.fallback
import coil3.request.placeholder
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.util.ext.enqueueWith import org.koitharu.kotatsu.core.util.ext.enqueueWith
import org.koitharu.kotatsu.core.util.ext.getAnimationDuration import org.koitharu.kotatsu.core.util.ext.getAnimationDuration
import org.koitharu.kotatsu.core.util.ext.getThemeColor import org.koitharu.kotatsu.core.util.ext.getThemeColor
import org.koitharu.kotatsu.core.util.ext.mangaSourceExtra
import org.koitharu.kotatsu.core.util.ext.newImageRequest import org.koitharu.kotatsu.core.util.ext.newImageRequest
import org.koitharu.kotatsu.core.util.ext.source
import org.koitharu.kotatsu.databinding.ItemCategoriesAllBinding import org.koitharu.kotatsu.databinding.ItemCategoriesAllBinding
import org.koitharu.kotatsu.databinding.ItemCategoryBinding import org.koitharu.kotatsu.databinding.ItemCategoryBinding
import org.koitharu.kotatsu.favourites.ui.categories.FavouriteCategoriesListListener import org.koitharu.kotatsu.favourites.ui.categories.FavouriteCategoriesListListener
@ -87,7 +92,7 @@ fun categoryAD(
coverViews[i].newImageRequest(lifecycleOwner, cover?.url)?.run { coverViews[i].newImageRequest(lifecycleOwner, cover?.url)?.run {
placeholder(R.drawable.ic_placeholder) placeholder(R.drawable.ic_placeholder)
fallback(fallback) fallback(fallback)
source(cover?.mangaSource) mangaSourceExtra(cover?.mangaSource)
crossfade(crossFadeDuration * (i + 1)) crossfade(crossFadeDuration * (i + 1))
error(R.drawable.ic_error_placeholder) error(R.drawable.ic_error_placeholder)
allowRgb565(true) allowRgb565(true)
@ -153,7 +158,7 @@ fun allCategoriesAD(
coverViews[i].newImageRequest(lifecycleOwner, cover?.url)?.run { coverViews[i].newImageRequest(lifecycleOwner, cover?.url)?.run {
placeholder(R.drawable.ic_placeholder) placeholder(R.drawable.ic_placeholder)
fallback(fallback) fallback(fallback)
source(cover?.mangaSource) mangaSourceExtra(cover?.mangaSource)
crossfade(crossFadeDuration * (i + 1)) crossfade(crossFadeDuration * (i + 1))
error(R.drawable.ic_error_placeholder) error(R.drawable.ic_error_placeholder)
allowRgb565(true) allowRgb565(true)

@ -7,7 +7,7 @@ import android.view.ViewGroup
import android.widget.Toast import android.widget.Toast
import androidx.fragment.app.FragmentManager import androidx.fragment.app.FragmentManager
import androidx.fragment.app.viewModels import androidx.fragment.app.viewModels
import coil.ImageLoader import coil3.ImageLoader
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener

@ -9,7 +9,12 @@ import androidx.core.graphics.ColorUtils
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.core.widget.ImageViewCompat import androidx.core.widget.ImageViewCompat
import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleOwner
import coil.ImageLoader import coil3.ImageLoader
import coil3.request.allowRgb565
import coil3.request.crossfade
import coil3.request.error
import coil3.request.fallback
import coil3.request.placeholder
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.util.ext.disposeImageRequest import org.koitharu.kotatsu.core.util.ext.disposeImageRequest
@ -17,8 +22,8 @@ import org.koitharu.kotatsu.core.util.ext.enqueueWith
import org.koitharu.kotatsu.core.util.ext.getAnimationDuration import org.koitharu.kotatsu.core.util.ext.getAnimationDuration
import org.koitharu.kotatsu.core.util.ext.getThemeColor import org.koitharu.kotatsu.core.util.ext.getThemeColor
import org.koitharu.kotatsu.core.util.ext.joinToStringWithLimit import org.koitharu.kotatsu.core.util.ext.joinToStringWithLimit
import org.koitharu.kotatsu.core.util.ext.mangaSourceExtra
import org.koitharu.kotatsu.core.util.ext.newImageRequest import org.koitharu.kotatsu.core.util.ext.newImageRequest
import org.koitharu.kotatsu.core.util.ext.source
import org.koitharu.kotatsu.databinding.ItemCategoriesHeaderBinding import org.koitharu.kotatsu.databinding.ItemCategoriesHeaderBinding
import org.koitharu.kotatsu.favourites.ui.categories.FavouriteCategoriesActivity import org.koitharu.kotatsu.favourites.ui.categories.FavouriteCategoriesActivity
import org.koitharu.kotatsu.favourites.ui.categories.edit.FavouritesCategoryEditActivity import org.koitharu.kotatsu.favourites.ui.categories.edit.FavouritesCategoryEditActivity
@ -74,7 +79,7 @@ fun categoriesHeaderAD(
view.newImageRequest(lifecycleOwner, cover.url)?.run { view.newImageRequest(lifecycleOwner, cover.url)?.run {
placeholder(R.drawable.ic_placeholder) placeholder(R.drawable.ic_placeholder)
fallback(fallback) fallback(fallback)
source(cover.mangaSource) mangaSourceExtra(cover.mangaSource)
crossfade(crossFadeDuration * (i + 1)) crossfade(crossFadeDuration * (i + 1))
error(R.drawable.ic_error_placeholder) error(R.drawable.ic_error_placeholder)
allowRgb565(true) allowRgb565(true)

@ -1,7 +1,7 @@
package org.koitharu.kotatsu.favourites.ui.categories.select.adapter package org.koitharu.kotatsu.favourites.ui.categories.select.adapter
import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleOwner
import coil.ImageLoader import coil3.ImageLoader
import org.koitharu.kotatsu.core.ui.BaseListAdapter import org.koitharu.kotatsu.core.ui.BaseListAdapter
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.favourites.ui.categories.select.model.MangaCategoryItem import org.koitharu.kotatsu.favourites.ui.categories.select.model.MangaCategoryItem

@ -12,7 +12,7 @@ import androidx.core.view.isGone
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.core.view.updatePadding import androidx.core.view.updatePadding
import androidx.fragment.app.viewModels import androidx.fragment.app.viewModels
import coil.ImageLoader import coil3.ImageLoader
import com.google.android.material.tabs.TabLayoutMediator import com.google.android.material.tabs.TabLayoutMediator
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R

@ -2,7 +2,7 @@ package org.koitharu.kotatsu.history.ui
import android.content.Context import android.content.Context
import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleOwner
import coil.ImageLoader import coil3.ImageLoader
import org.koitharu.kotatsu.core.ui.list.fastscroll.FastScroller import org.koitharu.kotatsu.core.ui.list.fastscroll.FastScroller
import org.koitharu.kotatsu.list.ui.adapter.MangaListAdapter import org.koitharu.kotatsu.list.ui.adapter.MangaListAdapter
import org.koitharu.kotatsu.list.ui.adapter.MangaListListener import org.koitharu.kotatsu.list.ui.adapter.MangaListListener

@ -13,12 +13,15 @@ import androidx.core.graphics.drawable.toBitmap
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.core.view.updateLayoutParams import androidx.core.view.updateLayoutParams
import androidx.swiperefreshlayout.widget.CircularProgressDrawable import androidx.swiperefreshlayout.widget.CircularProgressDrawable
import coil.ImageLoader import coil3.Image
import coil.request.CachePolicy import coil3.ImageLoader
import coil.request.ErrorResult import coil3.asDrawable
import coil.request.ImageRequest import coil3.request.CachePolicy
import coil.request.SuccessResult import coil3.request.ErrorResult
import coil.target.ViewTarget import coil3.request.ImageRequest
import coil3.request.SuccessResult
import coil3.request.lifecycle
import coil3.target.ViewTarget
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.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
@ -33,9 +36,9 @@ import org.koitharu.kotatsu.core.util.ext.enqueueWith
import org.koitharu.kotatsu.core.util.ext.getDisplayIcon import org.koitharu.kotatsu.core.util.ext.getDisplayIcon
import org.koitharu.kotatsu.core.util.ext.getDisplayMessage import org.koitharu.kotatsu.core.util.ext.getDisplayMessage
import org.koitharu.kotatsu.core.util.ext.getThemeColor import org.koitharu.kotatsu.core.util.ext.getThemeColor
import org.koitharu.kotatsu.core.util.ext.mangaSourceExtra
import org.koitharu.kotatsu.core.util.ext.observe import org.koitharu.kotatsu.core.util.ext.observe
import org.koitharu.kotatsu.core.util.ext.observeEvent import org.koitharu.kotatsu.core.util.ext.observeEvent
import org.koitharu.kotatsu.core.util.ext.source
import org.koitharu.kotatsu.databinding.ActivityImageBinding import org.koitharu.kotatsu.databinding.ActivityImageBinding
import org.koitharu.kotatsu.databinding.ItemErrorStateBinding import org.koitharu.kotatsu.databinding.ItemErrorStateBinding
import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.parsers.model.MangaSource
@ -120,7 +123,7 @@ class ImageActivity : BaseActivity<ActivityImageBinding>(), ImageRequest.Listene
.memoryCachePolicy(CachePolicy.DISABLED) .memoryCachePolicy(CachePolicy.DISABLED)
.lifecycle(this) .lifecycle(this)
.listener(this) .listener(this)
.source(MangaSource(intent.getStringExtra(EXTRA_SOURCE))) .mangaSourceExtra(MangaSource(intent.getStringExtra(EXTRA_SOURCE)))
.target(SsivTarget(viewBinding.ssiv)) .target(SsivTarget(viewBinding.ssiv))
.enqueueWith(coil) .enqueueWith(coil)
} }
@ -152,9 +155,9 @@ class ImageActivity : BaseActivity<ActivityImageBinding>(), ImageRequest.Listene
override val view: SubsamplingScaleImageView, override val view: SubsamplingScaleImageView,
) : ViewTarget<SubsamplingScaleImageView> { ) : ViewTarget<SubsamplingScaleImageView> {
override fun onError(error: Drawable?) = setDrawable(error) override fun onError(error: Image?) = setDrawable(error?.asDrawable(view.resources))
override fun onSuccess(result: Drawable) = setDrawable(result) override fun onSuccess(result: Image) = setDrawable(result.asDrawable(view.resources))
override fun equals(other: Any?): Boolean { override fun equals(other: Any?): Boolean {
return (this === other) || (other is SsivTarget && view == other.view) return (this === other) || (other is SsivTarget && view == other.view)

@ -11,8 +11,8 @@ import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.view.MenuProvider import androidx.core.view.MenuProvider
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.util.ext.isZipUri
import org.koitharu.kotatsu.core.util.ext.tryLaunch import org.koitharu.kotatsu.core.util.ext.tryLaunch
import org.koitharu.kotatsu.local.data.isZipUri
class ImageMenuProvider( class ImageMenuProvider(
private val activity: ComponentActivity, private val activity: ComponentActivity,

@ -5,9 +5,9 @@ import android.graphics.Bitmap
import android.net.Uri import android.net.Uri
import androidx.core.graphics.drawable.toBitmap import androidx.core.graphics.drawable.toBitmap
import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.SavedStateHandle
import coil.ImageLoader import coil3.ImageLoader
import coil.request.CachePolicy import coil3.request.CachePolicy
import coil.request.ImageRequest import coil3.request.ImageRequest
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@ -18,8 +18,8 @@ import org.koitharu.kotatsu.core.ui.BaseViewModel
import org.koitharu.kotatsu.core.util.ext.MutableEventFlow import org.koitharu.kotatsu.core.util.ext.MutableEventFlow
import org.koitharu.kotatsu.core.util.ext.call import org.koitharu.kotatsu.core.util.ext.call
import org.koitharu.kotatsu.core.util.ext.getDrawableOrThrow import org.koitharu.kotatsu.core.util.ext.getDrawableOrThrow
import org.koitharu.kotatsu.core.util.ext.mangaSourceExtra
import org.koitharu.kotatsu.core.util.ext.require import org.koitharu.kotatsu.core.util.ext.require
import org.koitharu.kotatsu.core.util.ext.source
import javax.inject.Inject import javax.inject.Inject
@HiltViewModel @HiltViewModel
@ -37,7 +37,7 @@ class ImageViewModel @Inject constructor(
.memoryCachePolicy(CachePolicy.READ_ONLY) .memoryCachePolicy(CachePolicy.READ_ONLY)
.data(savedStateHandle.require<Uri>(BaseActivity.EXTRA_DATA)) .data(savedStateHandle.require<Uri>(BaseActivity.EXTRA_DATA))
.memoryCachePolicy(CachePolicy.DISABLED) .memoryCachePolicy(CachePolicy.DISABLED)
.source(MangaSource(savedStateHandle[ImageActivity.EXTRA_SOURCE])) .mangaSourceExtra(MangaSource(savedStateHandle[ImageActivity.EXTRA_SOURCE]))
.build() .build()
val bitmap = coil.execute(request).getDrawableOrThrow().toBitmap() val bitmap = coil.execute(request).getDrawableOrThrow().toBitmap()
runInterruptible(Dispatchers.IO) { runInterruptible(Dispatchers.IO) {

@ -16,7 +16,7 @@ import androidx.core.view.updateLayoutParams
import androidx.core.view.updatePadding import androidx.core.view.updatePadding
import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.GridLayoutManager
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import coil.ImageLoader import coil3.ImageLoader
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R

@ -1,7 +1,7 @@
package org.koitharu.kotatsu.list.ui.adapter package org.koitharu.kotatsu.list.ui.adapter
import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleOwner
import coil.ImageLoader import coil3.ImageLoader
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
import org.koitharu.kotatsu.core.util.ext.enqueueWith import org.koitharu.kotatsu.core.util.ext.enqueueWith
import org.koitharu.kotatsu.core.util.ext.newImageRequest import org.koitharu.kotatsu.core.util.ext.newImageRequest

@ -1,7 +1,7 @@
package org.koitharu.kotatsu.list.ui.adapter package org.koitharu.kotatsu.list.ui.adapter
import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleOwner
import coil.ImageLoader import coil3.ImageLoader
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
import org.koitharu.kotatsu.core.util.ext.enqueueWith import org.koitharu.kotatsu.core.util.ext.enqueueWith
import org.koitharu.kotatsu.core.util.ext.newImageRequest import org.koitharu.kotatsu.core.util.ext.newImageRequest

@ -2,7 +2,9 @@ package org.koitharu.kotatsu.list.ui.adapter
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleOwner
import coil.ImageLoader import coil3.ImageLoader
import coil3.request.allowRgb565
import coil3.request.transformations
import com.google.android.material.badge.BadgeDrawable import com.google.android.material.badge.BadgeDrawable
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
import org.koitharu.kotatsu.core.ui.image.CoverSizeResolver import org.koitharu.kotatsu.core.ui.image.CoverSizeResolver
@ -11,8 +13,8 @@ import org.koitharu.kotatsu.core.ui.list.AdapterDelegateClickListenerAdapter
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.util.ext.defaultPlaceholders import org.koitharu.kotatsu.core.util.ext.defaultPlaceholders
import org.koitharu.kotatsu.core.util.ext.enqueueWith import org.koitharu.kotatsu.core.util.ext.enqueueWith
import org.koitharu.kotatsu.core.util.ext.mangaExtra
import org.koitharu.kotatsu.core.util.ext.newImageRequest import org.koitharu.kotatsu.core.util.ext.newImageRequest
import org.koitharu.kotatsu.core.util.ext.source
import org.koitharu.kotatsu.databinding.ItemMangaGridBinding import org.koitharu.kotatsu.databinding.ItemMangaGridBinding
import org.koitharu.kotatsu.list.ui.ListModelDiffCallback.Companion.PAYLOAD_PROGRESS_CHANGED import org.koitharu.kotatsu.list.ui.ListModelDiffCallback.Companion.PAYLOAD_PROGRESS_CHANGED
import org.koitharu.kotatsu.list.ui.model.ListModel import org.koitharu.kotatsu.list.ui.model.ListModel
@ -42,8 +44,7 @@ fun mangaGridItemAD(
defaultPlaceholders(context) defaultPlaceholders(context)
transformations(TrimTransformation()) transformations(TrimTransformation())
allowRgb565(true) allowRgb565(true)
tag(item.manga) mangaExtra(item.manga)
source(item.source)
enqueueWith(coil) enqueueWith(coil)
} }
badge = itemView.bindBadge(badge, item.counter) badge = itemView.bindBadge(badge, item.counter)

@ -1,7 +1,7 @@
package org.koitharu.kotatsu.list.ui.adapter package org.koitharu.kotatsu.list.ui.adapter
import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleOwner
import coil.ImageLoader import coil3.ImageLoader
import org.koitharu.kotatsu.core.ui.BaseListAdapter import org.koitharu.kotatsu.core.ui.BaseListAdapter
import org.koitharu.kotatsu.list.ui.model.ListModel import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.list.ui.size.ItemSizeResolver import org.koitharu.kotatsu.list.ui.size.ItemSizeResolver

@ -1,7 +1,9 @@
package org.koitharu.kotatsu.list.ui.adapter package org.koitharu.kotatsu.list.ui.adapter
import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleOwner
import coil.ImageLoader import coil3.ImageLoader
import coil3.request.allowRgb565
import coil3.request.transformations
import com.google.android.material.badge.BadgeDrawable import com.google.android.material.badge.BadgeDrawable
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
import org.koitharu.kotatsu.core.ui.image.CoverSizeResolver import org.koitharu.kotatsu.core.ui.image.CoverSizeResolver
@ -9,8 +11,8 @@ import org.koitharu.kotatsu.core.ui.image.TrimTransformation
import org.koitharu.kotatsu.core.ui.list.AdapterDelegateClickListenerAdapter import org.koitharu.kotatsu.core.ui.list.AdapterDelegateClickListenerAdapter
import org.koitharu.kotatsu.core.util.ext.defaultPlaceholders import org.koitharu.kotatsu.core.util.ext.defaultPlaceholders
import org.koitharu.kotatsu.core.util.ext.enqueueWith import org.koitharu.kotatsu.core.util.ext.enqueueWith
import org.koitharu.kotatsu.core.util.ext.mangaExtra
import org.koitharu.kotatsu.core.util.ext.newImageRequest import org.koitharu.kotatsu.core.util.ext.newImageRequest
import org.koitharu.kotatsu.core.util.ext.source
import org.koitharu.kotatsu.core.util.ext.textAndVisible import org.koitharu.kotatsu.core.util.ext.textAndVisible
import org.koitharu.kotatsu.databinding.ItemMangaListDetailsBinding import org.koitharu.kotatsu.databinding.ItemMangaListDetailsBinding
import org.koitharu.kotatsu.list.ui.ListModelDiffCallback import org.koitharu.kotatsu.list.ui.ListModelDiffCallback
@ -40,8 +42,7 @@ fun mangaListDetailedItemAD(
defaultPlaceholders(context) defaultPlaceholders(context)
transformations(TrimTransformation()) transformations(TrimTransformation())
allowRgb565(true) allowRgb565(true)
tag(item.manga) mangaExtra(item.manga)
source(item.source)
enqueueWith(coil) enqueueWith(coil)
} }
binding.textViewTags.text = item.tags.joinToString(separator = ", ") { it.title ?: "" } binding.textViewTags.text = item.tags.joinToString(separator = ", ") { it.title ?: "" }

@ -1,7 +1,9 @@
package org.koitharu.kotatsu.list.ui.adapter package org.koitharu.kotatsu.list.ui.adapter
import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleOwner
import coil.ImageLoader import coil3.ImageLoader
import coil3.request.allowRgb565
import coil3.request.transformations
import com.google.android.material.badge.BadgeDrawable import com.google.android.material.badge.BadgeDrawable
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
import org.koitharu.kotatsu.core.ui.image.TrimTransformation import org.koitharu.kotatsu.core.ui.image.TrimTransformation
@ -9,8 +11,8 @@ import org.koitharu.kotatsu.core.ui.list.AdapterDelegateClickListenerAdapter
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.util.ext.defaultPlaceholders import org.koitharu.kotatsu.core.util.ext.defaultPlaceholders
import org.koitharu.kotatsu.core.util.ext.enqueueWith import org.koitharu.kotatsu.core.util.ext.enqueueWith
import org.koitharu.kotatsu.core.util.ext.mangaExtra
import org.koitharu.kotatsu.core.util.ext.newImageRequest import org.koitharu.kotatsu.core.util.ext.newImageRequest
import org.koitharu.kotatsu.core.util.ext.source
import org.koitharu.kotatsu.core.util.ext.textAndVisible import org.koitharu.kotatsu.core.util.ext.textAndVisible
import org.koitharu.kotatsu.databinding.ItemMangaListBinding import org.koitharu.kotatsu.databinding.ItemMangaListBinding
import org.koitharu.kotatsu.list.ui.model.ListModel import org.koitharu.kotatsu.list.ui.model.ListModel
@ -35,8 +37,7 @@ fun mangaListItemAD(
defaultPlaceholders(context) defaultPlaceholders(context)
allowRgb565(true) allowRgb565(true)
transformations(TrimTransformation()) transformations(TrimTransformation())
tag(item.manga) mangaExtra(item.manga)
source(item.source)
enqueueWith(coil) enqueueWith(coil)
} }
badge = itemView.bindBadge(badge, item.counter) badge = itemView.bindBadge(badge, item.counter)

@ -9,10 +9,15 @@ import androidx.core.graphics.Insets
import androidx.core.text.method.LinkMovementMethodCompat import androidx.core.text.method.LinkMovementMethodCompat
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.fragment.app.viewModels import androidx.fragment.app.viewModels
import coil.ImageLoader import coil3.ImageLoader
import coil.request.ImageRequest import coil3.request.ImageRequest
import coil.request.SuccessResult import coil3.request.SuccessResult
import coil.util.CoilUtils import coil3.request.error
import coil3.request.fallback
import coil3.request.lifecycle
import coil3.request.placeholder
import coil3.request.target
import coil3.util.CoilUtils
import com.google.android.material.chip.Chip import com.google.android.material.chip.Chip
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
@ -21,8 +26,10 @@ import org.koitharu.kotatsu.core.ui.image.CoverSizeResolver
import org.koitharu.kotatsu.core.ui.widgets.ChipsView import org.koitharu.kotatsu.core.ui.widgets.ChipsView
import org.koitharu.kotatsu.core.util.ext.crossfade import org.koitharu.kotatsu.core.util.ext.crossfade
import org.koitharu.kotatsu.core.util.ext.defaultPlaceholders import org.koitharu.kotatsu.core.util.ext.defaultPlaceholders
import org.koitharu.kotatsu.core.util.ext.drawable
import org.koitharu.kotatsu.core.util.ext.enqueueWith import org.koitharu.kotatsu.core.util.ext.enqueueWith
import org.koitharu.kotatsu.core.util.ext.ifNullOrEmpty import org.koitharu.kotatsu.core.util.ext.ifNullOrEmpty
import org.koitharu.kotatsu.core.util.ext.mangaSourceExtra
import org.koitharu.kotatsu.core.util.ext.observe import org.koitharu.kotatsu.core.util.ext.observe
import org.koitharu.kotatsu.core.util.ext.scaleUpActivityOptionsOf import org.koitharu.kotatsu.core.util.ext.scaleUpActivityOptionsOf
import org.koitharu.kotatsu.core.util.ext.textAndVisible import org.koitharu.kotatsu.core.util.ext.textAndVisible
@ -169,7 +176,7 @@ class PreviewFragment : BaseFragment<FragmentPreviewBinding>(), View.OnClickList
.target(requireViewBinding().imageViewCover) .target(requireViewBinding().imageViewCover)
.size(CoverSizeResolver(requireViewBinding().imageViewCover)) .size(CoverSizeResolver(requireViewBinding().imageViewCover))
.data(imageUrl) .data(imageUrl)
.tag(manga.source) .mangaSourceExtra(manga.source)
.crossfade(requireContext()) .crossfade(requireContext())
.lifecycle(viewLifecycleOwner) .lifecycle(viewLifecycleOwner)
.placeholderMemoryCacheKey(manga.coverUrl) .placeholderMemoryCacheKey(manga.coverUrl)

@ -1,57 +0,0 @@
package org.koitharu.kotatsu.local.data
import android.net.Uri
import android.webkit.MimeTypeMap
import coil.ImageLoader
import coil.decode.DataSource
import coil.decode.ImageSource
import coil.fetch.Fetcher
import coil.fetch.SourceResult
import coil.request.Options
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runInterruptible
import okhttp3.internal.closeQuietly
import okio.buffer
import okio.source
import org.koitharu.kotatsu.local.data.util.withExtraCloseable
import java.util.zip.ZipFile
class CbzFetcher(
private val uri: Uri,
private val options: Options
) : Fetcher {
override suspend fun fetch() = runInterruptible(Dispatchers.IO) {
val zip = ZipFile(uri.schemeSpecificPart)
try {
val entry = zip.getEntry(uri.fragment)
val ext = MimeTypeMap.getFileExtensionFromUrl(entry.name)
val bufferedSource = zip.getInputStream(entry).source().withExtraCloseable(zip).buffer()
SourceResult(
source = ImageSource(
source = bufferedSource,
context = options.context,
metadata = CbzMetadata(uri),
),
mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(ext),
dataSource = DataSource.DISK,
)
} catch (e: Throwable) {
zip.closeQuietly()
throw e
}
}
class Factory : Fetcher.Factory<Uri> {
override fun create(data: Uri, options: Options, imageLoader: ImageLoader): Fetcher? {
return if (data.scheme == "cbz") {
CbzFetcher(data, options)
} else {
null
}
}
}
class CbzMetadata(val uri: Uri) : ImageSource.Metadata()
}

@ -1,7 +1,5 @@
package org.koitharu.kotatsu.local.data package org.koitharu.kotatsu.local.data
import android.net.Uri
import org.koitharu.kotatsu.core.util.ext.URI_SCHEME_ZIP
import java.io.File import java.io.File
private fun isCbzExtension(ext: String?): Boolean { private fun isCbzExtension(ext: String?): Boolean {
@ -14,9 +12,3 @@ fun hasCbzExtension(string: String): Boolean {
} }
fun File.hasCbzExtension() = isCbzExtension(extension) fun File.hasCbzExtension() = isCbzExtension(extension)
fun Uri.isZipUri() = scheme.let {
it == URI_SCHEME_ZIP || it == "cbz" || it == "zip"
}
fun Uri.isFileUri() = scheme == "file"

@ -19,9 +19,9 @@ import kotlinx.coroutines.runInterruptible
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import okhttp3.Cache import okhttp3.Cache
import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.util.ext.URI_SCHEME_FILE
import org.koitharu.kotatsu.core.util.ext.computeSize import org.koitharu.kotatsu.core.util.ext.computeSize
import org.koitharu.kotatsu.core.util.ext.getStorageName import org.koitharu.kotatsu.core.util.ext.getStorageName
import org.koitharu.kotatsu.core.util.ext.isFileUri
import org.koitharu.kotatsu.core.util.ext.resolveFile import org.koitharu.kotatsu.core.util.ext.resolveFile
import org.koitharu.kotatsu.parsers.util.mapToSet import org.koitharu.kotatsu.parsers.util.mapToSet
import java.io.File import java.io.File
@ -90,7 +90,7 @@ class LocalStorageManager @Inject constructor(
} }
suspend fun resolveUri(uri: Uri): File? = runInterruptible(Dispatchers.IO) { suspend fun resolveUri(uri: Uri): File? = runInterruptible(Dispatchers.IO) {
if (uri.scheme == URI_SCHEME_FILE) { if (uri.isFileUri()) {
uri.toFile() uri.toFile()
} else { } else {
uri.resolveFile(context) uri.resolveFile(context)

@ -7,6 +7,7 @@ import kotlinx.coroutines.flow.channelFlow
import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.koitharu.kotatsu.core.util.ext.toZipUri
import org.koitharu.kotatsu.local.data.hasCbzExtension import org.koitharu.kotatsu.local.data.hasCbzExtension
import org.koitharu.kotatsu.local.domain.model.LocalManga import org.koitharu.kotatsu.local.domain.model.LocalManga
import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.Manga
@ -64,8 +65,7 @@ sealed class LocalMangaInput(
}.flowOn(Dispatchers.Default).firstOrNull() }.flowOn(Dispatchers.Default).firstOrNull()
@JvmStatic @JvmStatic
protected fun zipUri(file: File, entryName: String): String = protected fun zipUri(file: File, entryName: String): String = file.toZipUri(entryName).toString()
Uri.fromParts("cbz", file.path, entryName).toString()
@JvmStatic @JvmStatic
protected fun Manga.copy2( protected fun Manga.copy2(

@ -1,21 +0,0 @@
package org.koitharu.kotatsu.local.data.util
import okhttp3.internal.closeQuietly
import okio.Closeable
import okio.Source
private class ExtraCloseableSource(
private val delegate: Source,
private val extraCloseable: Closeable,
) : Source by delegate {
override fun close() {
try {
delegate.close()
} finally {
extraCloseable.closeQuietly()
}
}
}
fun Source.withExtraCloseable(closeable: Closeable): Source = ExtraCloseableSource(this, closeable)

@ -13,8 +13,8 @@ import androidx.core.app.NotificationManagerCompat
import androidx.core.app.PendingIntentCompat import androidx.core.app.PendingIntentCompat
import androidx.core.app.ServiceCompat import androidx.core.app.ServiceCompat
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import coil.ImageLoader import coil3.ImageLoader
import coil.request.ImageRequest import coil3.request.ImageRequest
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
@ -22,6 +22,7 @@ import org.koitharu.kotatsu.core.ErrorReporterReceiver
import org.koitharu.kotatsu.core.ui.CoroutineIntentService import org.koitharu.kotatsu.core.ui.CoroutineIntentService
import org.koitharu.kotatsu.core.util.ext.checkNotificationPermission import org.koitharu.kotatsu.core.util.ext.checkNotificationPermission
import org.koitharu.kotatsu.core.util.ext.getDisplayMessage import org.koitharu.kotatsu.core.util.ext.getDisplayMessage
import org.koitharu.kotatsu.core.util.ext.mangaSourceExtra
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
import org.koitharu.kotatsu.core.util.ext.toBitmapOrNull import org.koitharu.kotatsu.core.util.ext.toBitmapOrNull
import org.koitharu.kotatsu.core.util.ext.toUriOrNull import org.koitharu.kotatsu.core.util.ext.toUriOrNull
@ -113,7 +114,7 @@ class ImportService : CoroutineIntentService() {
coil.execute( coil.execute(
ImageRequest.Builder(applicationContext) ImageRequest.Builder(applicationContext)
.data(manga.coverUrl) .data(manga.coverUrl)
.tag(manga.source) .mangaSourceExtra(manga.source)
.build(), .build(),
).toBitmapOrNull(), ).toBitmapOrNull(),
) )

@ -1,10 +1,10 @@
package org.koitharu.kotatsu.main.domain package org.koitharu.kotatsu.main.domain
import androidx.collection.ArraySet import androidx.collection.ArraySet
import coil.intercept.Interceptor import coil3.intercept.Interceptor
import coil.network.HttpException import coil3.network.HttpException
import coil.request.ErrorResult import coil3.request.ErrorResult
import coil.request.ImageResult import coil3.request.ImageResult
import okio.FileNotFoundException import okio.FileNotFoundException
import org.jsoup.HttpStatusException import org.jsoup.HttpStatusException
import org.koitharu.kotatsu.bookmarks.domain.Bookmark import org.koitharu.kotatsu.bookmarks.domain.Bookmark
@ -13,7 +13,9 @@ import org.koitharu.kotatsu.core.model.findById
import org.koitharu.kotatsu.core.model.isLocal import org.koitharu.kotatsu.core.model.isLocal
import org.koitharu.kotatsu.core.parser.MangaDataRepository import org.koitharu.kotatsu.core.parser.MangaDataRepository
import org.koitharu.kotatsu.core.parser.MangaRepository import org.koitharu.kotatsu.core.parser.MangaRepository
import org.koitharu.kotatsu.core.util.ext.bookmarkKey
import org.koitharu.kotatsu.core.util.ext.ifNullOrEmpty import org.koitharu.kotatsu.core.util.ext.ifNullOrEmpty
import org.koitharu.kotatsu.core.util.ext.mangaKey
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
import org.koitharu.kotatsu.parsers.exception.ParseException import org.koitharu.kotatsu.parsers.exception.ParseException
import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.Manga
@ -33,18 +35,18 @@ class CoverRestoreInterceptor @Inject constructor(
override suspend fun intercept(chain: Interceptor.Chain): ImageResult { override suspend fun intercept(chain: Interceptor.Chain): ImageResult {
val request = chain.request val request = chain.request
val result = chain.proceed(request) val result = chain.proceed()
if (result is ErrorResult && result.throwable.shouldRestore()) { if (result is ErrorResult && result.throwable.shouldRestore()) {
request.tags.tag<Manga>()?.let { request.extras[mangaKey]?.let {
if (restoreManga(it)) { if (restoreManga(it)) {
return chain.proceed(request.newBuilder().build()) return chain.withRequest(request.newBuilder().build()).proceed()
} else { } else {
return result return result
} }
} }
request.tags.tag<Bookmark>()?.let { request.extras[bookmarkKey]?.let {
if (restoreBookmark(it)) { if (restoreBookmark(it)) {
return chain.proceed(request.newBuilder().build()) return chain.withRequest(request.newBuilder().build()).proceed()
} else { } else {
return result return result
} }

@ -14,9 +14,9 @@ import org.koitharu.kotatsu.core.parser.MangaDataRepository
import org.koitharu.kotatsu.core.parser.MangaRepository import org.koitharu.kotatsu.core.parser.MangaRepository
import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.prefs.ReaderMode import org.koitharu.kotatsu.core.prefs.ReaderMode
import org.koitharu.kotatsu.core.util.ext.isFileUri
import org.koitharu.kotatsu.core.util.ext.isZipUri
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
import org.koitharu.kotatsu.local.data.isFileUri
import org.koitharu.kotatsu.local.data.isZipUri
import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaPage import org.koitharu.kotatsu.parsers.model.MangaPage
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable import org.koitharu.kotatsu.parsers.util.runCatchingCancellable

@ -1,10 +1,8 @@
package org.koitharu.kotatsu.reader.domain package org.koitharu.kotatsu.reader.domain
import android.content.ContentResolver.MimeTypeInfo
import android.content.Context import android.content.Context
import android.graphics.Rect import android.graphics.Rect
import android.net.Uri import android.net.Uri
import android.webkit.MimeTypeMap
import androidx.annotation.AnyThread import androidx.annotation.AnyThread
import androidx.collection.LongSparseArray import androidx.collection.LongSparseArray
import androidx.collection.set import androidx.collection.set
@ -29,6 +27,8 @@ import kotlinx.coroutines.sync.withPermit
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.Request import okhttp3.Request
import okio.use import okio.use
import org.jetbrains.annotations.Blocking
import org.koitharu.kotatsu.core.image.BitmapDecoderCompat
import org.koitharu.kotatsu.core.network.CommonHeaders import org.koitharu.kotatsu.core.network.CommonHeaders
import org.koitharu.kotatsu.core.network.MangaHttpClient import org.koitharu.kotatsu.core.network.MangaHttpClient
import org.koitharu.kotatsu.core.network.imageproxy.ImageProxyInterceptor import org.koitharu.kotatsu.core.network.imageproxy.ImageProxyInterceptor
@ -42,26 +42,25 @@ import org.koitharu.kotatsu.core.util.ext.cancelChildrenAndJoin
import org.koitharu.kotatsu.core.util.ext.compressToPNG import org.koitharu.kotatsu.core.util.ext.compressToPNG
import org.koitharu.kotatsu.core.util.ext.ensureRamAtLeast import org.koitharu.kotatsu.core.util.ext.ensureRamAtLeast
import org.koitharu.kotatsu.core.util.ext.ensureSuccess import org.koitharu.kotatsu.core.util.ext.ensureSuccess
import org.koitharu.kotatsu.core.util.ext.exists
import org.koitharu.kotatsu.core.util.ext.getCompletionResultOrNull import org.koitharu.kotatsu.core.util.ext.getCompletionResultOrNull
import org.koitharu.kotatsu.core.util.ext.isFileUri
import org.koitharu.kotatsu.core.util.ext.isNotEmpty
import org.koitharu.kotatsu.core.util.ext.isPowerSaveMode import org.koitharu.kotatsu.core.util.ext.isPowerSaveMode
import org.koitharu.kotatsu.core.util.ext.isTargetNotEmpty import org.koitharu.kotatsu.core.util.ext.isZipUri
import org.koitharu.kotatsu.core.util.ext.mimeType
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
import org.koitharu.kotatsu.core.util.ext.ramAvailable import org.koitharu.kotatsu.core.util.ext.ramAvailable
import org.koitharu.kotatsu.core.util.ext.use import org.koitharu.kotatsu.core.util.ext.use
import org.koitharu.kotatsu.core.util.ext.withProgress import org.koitharu.kotatsu.core.util.ext.withProgress
import org.koitharu.kotatsu.core.util.progress.ProgressDeferred import org.koitharu.kotatsu.core.util.progress.ProgressDeferred
import org.koitharu.kotatsu.local.data.PagesCache import org.koitharu.kotatsu.local.data.PagesCache
import org.koitharu.kotatsu.local.data.isFileUri
import org.koitharu.kotatsu.local.data.isZipUri
import org.koitharu.kotatsu.parsers.model.MangaPage import org.koitharu.kotatsu.parsers.model.MangaPage
import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.util.mimeType import org.koitharu.kotatsu.parsers.util.mimeType
import org.koitharu.kotatsu.parsers.util.requireBody import org.koitharu.kotatsu.parsers.util.requireBody
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
import org.koitharu.kotatsu.core.image.BitmapDecoderCompat
import org.koitharu.kotatsu.core.util.ext.mimeType
import org.koitharu.kotatsu.reader.ui.pager.ReaderPage import org.koitharu.kotatsu.reader.ui.pager.ReaderPage
import java.io.File
import java.util.LinkedList import java.util.LinkedList
import java.util.concurrent.atomic.AtomicInteger import java.util.concurrent.atomic.AtomicInteger
import java.util.zip.ZipFile import java.util.zip.ZipFile
@ -276,5 +275,28 @@ class PageLoader @Inject constructor(
.cacheControl(CommonHeaders.CACHE_CONTROL_NO_STORE) .cacheControl(CommonHeaders.CACHE_CONTROL_NO_STORE)
.tag(MangaSource::class.java, mangaSource) .tag(MangaSource::class.java, mangaSource)
.build() .build()
@Blocking
private fun Uri.exists(): Boolean = when {
isFileUri() -> toFile().exists()
isZipUri() -> {
val file = File(requireNotNull(schemeSpecificPart))
file.exists() && ZipFile(file).use { it.getEntry(fragment) != null }
}
else -> false
}
@Blocking
private fun Uri.isTargetNotEmpty(): Boolean = when {
isFileUri() -> toFile().isNotEmpty()
isZipUri() -> {
val file = File(requireNotNull(schemeSpecificPart))
file.exists() && ZipFile(file).use { (it.getEntry(fragment)?.size ?: 0L) != 0L }
}
else -> false
}
} }
} }

@ -5,6 +5,7 @@ import android.graphics.BitmapFactory
import android.net.Uri import android.net.Uri
import android.webkit.MimeTypeMap import android.webkit.MimeTypeMap
import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.ActivityResultLauncher
import androidx.core.net.toFile
import androidx.core.net.toUri import androidx.core.net.toUri
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@ -12,11 +13,17 @@ import kotlinx.coroutines.runInterruptible
import kotlinx.coroutines.suspendCancellableCoroutine import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import okhttp3.HttpUrl.Companion.toHttpUrl import okhttp3.HttpUrl.Companion.toHttpUrl
import okio.FileSystem
import okio.IOException import okio.IOException
import okio.Path.Companion.toPath
import okio.Source
import okio.buffer import okio.buffer
import okio.openZip
import okio.sink import okio.sink
import okio.source
import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.util.ext.source import org.koitharu.kotatsu.core.util.ext.isFileUri
import org.koitharu.kotatsu.core.util.ext.isZipUri
import org.koitharu.kotatsu.core.util.ext.toFileOrNull import org.koitharu.kotatsu.core.util.ext.toFileOrNull
import org.koitharu.kotatsu.core.util.ext.writeAllCancellable import org.koitharu.kotatsu.core.util.ext.writeAllCancellable
import org.koitharu.kotatsu.parsers.model.MangaPage import org.koitharu.kotatsu.parsers.model.MangaPage
@ -50,7 +57,7 @@ class PageSaveHelper @Inject constructor(
runInterruptible(Dispatchers.IO) { runInterruptible(Dispatchers.IO) {
contentResolver.openOutputStream(destination)?.sink()?.buffer() contentResolver.openOutputStream(destination)?.sink()?.buffer()
}?.use { output -> }?.use { output ->
pageUri.source().use { input -> getSource(pageUri).use { input ->
output.writeAllCancellable(input) output.writeAllCancellable(input)
} }
} ?: throw IOException("Output stream is null") } ?: throw IOException("Output stream is null")
@ -68,6 +75,14 @@ class PageSaveHelper @Inject constructor(
} }
} }
private fun getSource(uri: Uri): Source = when {
uri.isFileUri() -> uri.toFile().source()
uri.isZipUri() -> FileSystem.SYSTEM.openZip(uri.schemeSpecificPart.toPath())
.source(requireNotNull(uri.fragment).toPath())
else -> throw IllegalArgumentException("Bad uri $uri: unsupported scheme")
}
private suspend fun pickFileUri(saveLauncher: ActivityResultLauncher<String>, proposedName: String): Uri { private suspend fun pickFileUri(saveLauncher: ActivityResultLauncher<String>, proposedName: String): Uri {
val defaultUri = settings.getPagesSaveDir(context)?.uri?.buildUpon()?.appendPath(proposedName)?.toString() val defaultUri = settings.getPagesSaveDir(context)?.uri?.buildUpon()?.appendPath(proposedName)?.toString()
return withContext(Dispatchers.Main) { return withContext(Dispatchers.Main) {

@ -12,10 +12,12 @@ import androidx.activity.viewModels
import androidx.core.graphics.Insets import androidx.core.graphics.Insets
import androidx.core.view.updateLayoutParams import androidx.core.view.updateLayoutParams
import androidx.core.view.updatePadding import androidx.core.view.updatePadding
import coil.ImageLoader import coil3.ImageLoader
import coil.request.ImageRequest import coil3.request.ImageRequest
import coil.size.Scale import coil3.request.bitmapConfig
import coil.size.ViewSizeResolver import coil3.request.error
import coil3.size.Scale
import coil3.size.ViewSizeResolver
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.slider.LabelFormatter import com.google.android.material.slider.LabelFormatter
import com.google.android.material.slider.Slider import com.google.android.material.slider.Slider
@ -27,6 +29,7 @@ import org.koitharu.kotatsu.core.ui.BaseActivity
import org.koitharu.kotatsu.core.util.ext.decodeRegion import org.koitharu.kotatsu.core.util.ext.decodeRegion
import org.koitharu.kotatsu.core.util.ext.enqueueWith import org.koitharu.kotatsu.core.util.ext.enqueueWith
import org.koitharu.kotatsu.core.util.ext.indicator import org.koitharu.kotatsu.core.util.ext.indicator
import org.koitharu.kotatsu.core.util.ext.mangaSourceExtra
import org.koitharu.kotatsu.core.util.ext.observe import org.koitharu.kotatsu.core.util.ext.observe
import org.koitharu.kotatsu.core.util.ext.observeEvent import org.koitharu.kotatsu.core.util.ext.observeEvent
import org.koitharu.kotatsu.core.util.ext.setChecked import org.koitharu.kotatsu.core.util.ext.setChecked
@ -139,7 +142,7 @@ class ColorFilterConfigActivity :
.data(data) .data(data)
.scale(Scale.FILL) .scale(Scale.FILL)
.decodeRegion() .decodeRegion()
.tag(page.source) .mangaSourceExtra(page.source)
.bitmapConfig(if (viewModel.is32BitColorsEnabled) Bitmap.Config.ARGB_8888 else Bitmap.Config.RGB_565) .bitmapConfig(if (viewModel.is32BitColorsEnabled) Bitmap.Config.ARGB_8888 else Bitmap.Config.RGB_565)
.indicator(listOf(viewBinding.progressBefore, viewBinding.progressAfter)) .indicator(listOf(viewBinding.progressBefore, viewBinding.progressAfter))
.error(R.drawable.ic_error_placeholder) .error(R.drawable.ic_error_placeholder)

@ -2,7 +2,7 @@ package org.koitharu.kotatsu.reader.ui.colorfilter
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
import android.widget.ImageView import android.widget.ImageView
import coil.target.ImageViewTarget import coil3.target.ImageViewTarget
class DoubleViewTarget( class DoubleViewTarget(
primaryView: ImageView, primaryView: ImageView,

@ -7,7 +7,10 @@ import android.view.View
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.core.graphics.Insets import androidx.core.graphics.Insets
import androidx.core.view.updatePadding import androidx.core.view.updatePadding
import coil.ImageLoader import coil3.ImageLoader
import coil3.request.error
import coil3.request.fallback
import coil3.request.placeholder
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R

@ -1,7 +1,7 @@
package org.koitharu.kotatsu.scrobbling.common.ui.config.adapter package org.koitharu.kotatsu.scrobbling.common.ui.config.adapter
import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleOwner
import coil.ImageLoader import coil3.ImageLoader
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
import org.koitharu.kotatsu.core.ui.list.AdapterDelegateClickListenerAdapter import org.koitharu.kotatsu.core.ui.list.AdapterDelegateClickListenerAdapter
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener

@ -1,7 +1,7 @@
package org.koitharu.kotatsu.scrobbling.common.ui.config.adapter package org.koitharu.kotatsu.scrobbling.common.ui.config.adapter
import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleOwner
import coil.ImageLoader import coil3.ImageLoader
import org.koitharu.kotatsu.core.ui.BaseListAdapter import org.koitharu.kotatsu.core.ui.BaseListAdapter
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.list.ui.adapter.ListItemType import org.koitharu.kotatsu.list.ui.adapter.ListItemType

@ -11,7 +11,7 @@ import androidx.fragment.app.FragmentManager
import androidx.fragment.app.viewModels import androidx.fragment.app.viewModels
import androidx.recyclerview.widget.AsyncListDiffer import androidx.recyclerview.widget.AsyncListDiffer
import androidx.recyclerview.widget.RecyclerView.NO_ID import androidx.recyclerview.widget.RecyclerView.NO_ID
import coil.ImageLoader import coil3.ImageLoader
import com.google.android.material.tabs.TabLayout import com.google.android.material.tabs.TabLayout
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.launch import kotlinx.coroutines.launch

@ -1,7 +1,7 @@
package org.koitharu.kotatsu.scrobbling.common.ui.selector.adapter package org.koitharu.kotatsu.scrobbling.common.ui.selector.adapter
import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleOwner
import coil.ImageLoader import coil3.ImageLoader
import org.koitharu.kotatsu.core.ui.BaseListAdapter import org.koitharu.kotatsu.core.ui.BaseListAdapter
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.list.ui.adapter.ListItemType import org.koitharu.kotatsu.list.ui.adapter.ListItemType

@ -1,7 +1,8 @@
package org.koitharu.kotatsu.scrobbling.common.ui.selector.adapter package org.koitharu.kotatsu.scrobbling.common.ui.selector.adapter
import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleOwner
import coil.ImageLoader import coil3.ImageLoader
import coil3.request.allowRgb565
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.util.ext.defaultPlaceholders import org.koitharu.kotatsu.core.util.ext.defaultPlaceholders

@ -11,7 +11,7 @@ import androidx.activity.viewModels
import androidx.appcompat.view.ActionMode import androidx.appcompat.view.ActionMode
import androidx.core.graphics.Insets import androidx.core.graphics.Insets
import androidx.core.view.updatePadding import androidx.core.view.updatePadding
import coil.ImageLoader import coil3.ImageLoader
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.exceptions.resolve.SnackbarErrorObserver import org.koitharu.kotatsu.core.exceptions.resolve.SnackbarErrorObserver

@ -3,7 +3,7 @@ package org.koitharu.kotatsu.search.ui.multi.adapter
import android.content.Context import android.content.Context
import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleOwner
import androidx.recyclerview.widget.RecyclerView.RecycledViewPool import androidx.recyclerview.widget.RecyclerView.RecycledViewPool
import coil.ImageLoader import coil3.ImageLoader
import org.koitharu.kotatsu.core.ui.BaseListAdapter import org.koitharu.kotatsu.core.ui.BaseListAdapter
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.ui.list.fastscroll.FastScroller import org.koitharu.kotatsu.core.ui.list.fastscroll.FastScroller

@ -4,7 +4,7 @@ import androidx.core.view.isGone
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleOwner
import androidx.recyclerview.widget.RecyclerView.RecycledViewPool import androidx.recyclerview.widget.RecyclerView.RecycledViewPool
import coil.ImageLoader import coil3.ImageLoader
import com.hannesdorfmann.adapterdelegates4.ListDelegationAdapter import com.hannesdorfmann.adapterdelegates4.ListDelegationAdapter
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R

@ -7,7 +7,7 @@ import androidx.core.graphics.Insets
import androidx.core.view.updatePadding import androidx.core.view.updatePadding
import androidx.fragment.app.activityViewModels import androidx.fragment.app.activityViewModels
import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.ItemTouchHelper
import coil.ImageLoader import coil3.ImageLoader
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.os.VoiceInputContract import org.koitharu.kotatsu.core.os.VoiceInputContract
@ -74,11 +74,12 @@ class SearchSuggestionFragment :
companion object { companion object {
@Deprecated("", @Deprecated(
"",
ReplaceWith( ReplaceWith(
"SearchSuggestionFragment()", "SearchSuggestionFragment()",
"org.koitharu.kotatsu.search.ui.suggestion.SearchSuggestionFragment" "org.koitharu.kotatsu.search.ui.suggestion.SearchSuggestionFragment",
) ),
) )
fun newInstance() = SearchSuggestionFragment() fun newInstance() = SearchSuggestionFragment()
} }

@ -1,7 +1,7 @@
package org.koitharu.kotatsu.search.ui.suggestion.adapter package org.koitharu.kotatsu.search.ui.suggestion.adapter
import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleOwner
import coil.ImageLoader import coil3.ImageLoader
import org.koitharu.kotatsu.core.ui.BaseListAdapter import org.koitharu.kotatsu.core.ui.BaseListAdapter
import org.koitharu.kotatsu.search.ui.suggestion.SearchSuggestionListener import org.koitharu.kotatsu.search.ui.suggestion.SearchSuggestionListener
import org.koitharu.kotatsu.search.ui.suggestion.model.SearchSuggestionItem import org.koitharu.kotatsu.search.ui.suggestion.model.SearchSuggestionItem

@ -1,7 +1,10 @@
package org.koitharu.kotatsu.search.ui.suggestion.adapter package org.koitharu.kotatsu.search.ui.suggestion.adapter
import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleOwner
import coil.ImageLoader import coil3.ImageLoader
import coil3.request.error
import coil3.request.fallback
import coil3.request.placeholder
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.model.getSummary import org.koitharu.kotatsu.core.model.getSummary
@ -10,8 +13,8 @@ import org.koitharu.kotatsu.core.parser.favicon.faviconUri
import org.koitharu.kotatsu.core.ui.image.AnimatedFaviconDrawable import org.koitharu.kotatsu.core.ui.image.AnimatedFaviconDrawable
import org.koitharu.kotatsu.core.ui.image.FaviconDrawable import org.koitharu.kotatsu.core.ui.image.FaviconDrawable
import org.koitharu.kotatsu.core.util.ext.enqueueWith import org.koitharu.kotatsu.core.util.ext.enqueueWith
import org.koitharu.kotatsu.core.util.ext.mangaSourceExtra
import org.koitharu.kotatsu.core.util.ext.newImageRequest import org.koitharu.kotatsu.core.util.ext.newImageRequest
import org.koitharu.kotatsu.core.util.ext.source
import org.koitharu.kotatsu.databinding.ItemSearchSuggestionSourceBinding import org.koitharu.kotatsu.databinding.ItemSearchSuggestionSourceBinding
import org.koitharu.kotatsu.search.ui.suggestion.SearchSuggestionListener import org.koitharu.kotatsu.search.ui.suggestion.SearchSuggestionListener
import org.koitharu.kotatsu.search.ui.suggestion.model.SearchSuggestionItem import org.koitharu.kotatsu.search.ui.suggestion.model.SearchSuggestionItem
@ -40,7 +43,7 @@ fun searchSuggestionSourceAD(
fallback(fallbackIcon) fallback(fallbackIcon)
placeholder(AnimatedFaviconDrawable(context, R.style.FaviconDrawable_Small, item.source.name)) placeholder(AnimatedFaviconDrawable(context, R.style.FaviconDrawable_Small, item.source.name))
error(fallbackIcon) error(fallbackIcon)
source(item.source) mangaSourceExtra(item.source)
enqueueWith(coil) enqueueWith(coil)
} }
} }

@ -1,7 +1,10 @@
package org.koitharu.kotatsu.search.ui.suggestion.adapter package org.koitharu.kotatsu.search.ui.suggestion.adapter
import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleOwner
import coil.ImageLoader import coil3.ImageLoader
import coil3.request.error
import coil3.request.fallback
import coil3.request.placeholder
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.model.getSummary import org.koitharu.kotatsu.core.model.getSummary
@ -10,8 +13,8 @@ import org.koitharu.kotatsu.core.parser.favicon.faviconUri
import org.koitharu.kotatsu.core.ui.image.AnimatedFaviconDrawable import org.koitharu.kotatsu.core.ui.image.AnimatedFaviconDrawable
import org.koitharu.kotatsu.core.ui.image.FaviconDrawable import org.koitharu.kotatsu.core.ui.image.FaviconDrawable
import org.koitharu.kotatsu.core.util.ext.enqueueWith import org.koitharu.kotatsu.core.util.ext.enqueueWith
import org.koitharu.kotatsu.core.util.ext.mangaSourceExtra
import org.koitharu.kotatsu.core.util.ext.newImageRequest import org.koitharu.kotatsu.core.util.ext.newImageRequest
import org.koitharu.kotatsu.core.util.ext.source
import org.koitharu.kotatsu.databinding.ItemSearchSuggestionSourceTipBinding import org.koitharu.kotatsu.databinding.ItemSearchSuggestionSourceTipBinding
import org.koitharu.kotatsu.search.ui.suggestion.SearchSuggestionListener import org.koitharu.kotatsu.search.ui.suggestion.SearchSuggestionListener
import org.koitharu.kotatsu.search.ui.suggestion.model.SearchSuggestionItem import org.koitharu.kotatsu.search.ui.suggestion.model.SearchSuggestionItem
@ -37,7 +40,7 @@ fun searchSuggestionSourceTipAD(
fallback(fallbackIcon) fallback(fallbackIcon)
placeholder(AnimatedFaviconDrawable(context, R.style.FaviconDrawable_Small, item.source.name)) placeholder(AnimatedFaviconDrawable(context, R.style.FaviconDrawable_Small, item.source.name))
error(fallbackIcon) error(fallbackIcon)
source(item.source) mangaSourceExtra(item.source)
enqueueWith(coil) enqueueWith(coil)
} }
} }

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save