Fix images memory caching
This commit is contained in:
@@ -16,8 +16,8 @@ android {
|
||||
applicationId 'org.koitharu.kotatsu'
|
||||
minSdk = 21
|
||||
targetSdk = 34
|
||||
versionCode = 636
|
||||
versionName = '7.0-b2'
|
||||
versionCode = 637
|
||||
versionName = '7.0-b3'
|
||||
generatedDensities = []
|
||||
testInstrumentationRunner 'org.koitharu.kotatsu.HiltTestRunner'
|
||||
ksp {
|
||||
|
||||
@@ -43,6 +43,7 @@ import org.koitharu.kotatsu.core.util.AcraScreenLogger
|
||||
import org.koitharu.kotatsu.core.util.ext.connectivityManager
|
||||
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.MangaPageKeyer
|
||||
import org.koitharu.kotatsu.local.data.CacheDir
|
||||
import org.koitharu.kotatsu.local.data.CbzFetcher
|
||||
import org.koitharu.kotatsu.local.data.LocalStorageChanges
|
||||
@@ -101,8 +102,8 @@ interface AppModule {
|
||||
return ImageLoader.Builder(context)
|
||||
.okHttpClient(okHttpClient.newBuilder().cache(null).build())
|
||||
.interceptorDispatcher(Dispatchers.Default)
|
||||
.fetcherDispatcher(Dispatchers.IO)
|
||||
.decoderDispatcher(Dispatchers.Default)
|
||||
.fetcherDispatcher(Dispatchers.Default)
|
||||
.decoderDispatcher(Dispatchers.IO)
|
||||
.transformationDispatcher(Dispatchers.Default)
|
||||
.diskCache(diskCacheFactory)
|
||||
.logger(if (BuildConfig.DEBUG) DebugLogger() else null)
|
||||
@@ -113,6 +114,7 @@ interface AppModule {
|
||||
.add(SvgDecoder.Factory())
|
||||
.add(CbzFetcher.Factory())
|
||||
.add(FaviconFetcher.Factory(context, okHttpClient, mangaRepositoryFactory))
|
||||
.add(MangaPageKeyer())
|
||||
.add(pageFetcherFactory)
|
||||
.add(imageProxyInterceptor)
|
||||
.add(coverRestoreInterceptor)
|
||||
|
||||
@@ -7,23 +7,23 @@ import android.graphics.ColorFilter
|
||||
import android.graphics.PixelFormat
|
||||
import android.graphics.drawable.Animatable
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.view.animation.AccelerateDecelerateInterpolator
|
||||
import androidx.core.graphics.ColorUtils
|
||||
import androidx.interpolator.view.animation.FastOutSlowInInterpolator
|
||||
import com.google.android.material.animation.ArgbEvaluatorCompat
|
||||
import org.koitharu.kotatsu.core.util.ext.animatorDurationScale
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.util.ext.getAnimationDuration
|
||||
import org.koitharu.kotatsu.core.util.ext.getThemeColor
|
||||
import kotlin.math.abs
|
||||
import com.google.android.material.R as materialR
|
||||
|
||||
class AnimatedPlaceholderDrawable(context: Context) : Drawable(), Animatable, TimeAnimator.TimeListener {
|
||||
|
||||
private val colorLow = context.getThemeColor(materialR.attr.colorBackgroundFloating)
|
||||
private val colorHigh = context.getThemeColor(materialR.attr.colorSurfaceContainer)
|
||||
private val colorLow = context.getThemeColor(materialR.attr.colorSurfaceContainerLowest)
|
||||
private val colorHigh = context.getThemeColor(materialR.attr.colorSurfaceContainerHighest)
|
||||
private var currentColor: Int = colorLow
|
||||
private var alpha: Int = 255
|
||||
private val interpolator = FastOutSlowInInterpolator()
|
||||
private val period = 2000 * context.animatorDurationScale
|
||||
private val period = context.getAnimationDuration(R.integer.config_longAnimTime) * 2
|
||||
private val timeAnimator = TimeAnimator()
|
||||
|
||||
init {
|
||||
@@ -40,23 +40,21 @@ class AnimatedPlaceholderDrawable(context: Context) : Drawable(), Animatable, Ti
|
||||
}
|
||||
|
||||
override fun setAlpha(alpha: Int) {
|
||||
this.alpha = alpha
|
||||
// this.alpha = alpha FIXME coil's crossfade
|
||||
}
|
||||
|
||||
override fun setColorFilter(colorFilter: ColorFilter?) {
|
||||
throw UnsupportedOperationException("ColorFilter is not supported by PlaceholderDrawable")
|
||||
}
|
||||
override fun setColorFilter(colorFilter: ColorFilter?) = Unit
|
||||
|
||||
@Deprecated("Deprecated in Java")
|
||||
override fun getOpacity(): Int = PixelFormat.OPAQUE
|
||||
override fun getOpacity(): Int = if (alpha == 255) PixelFormat.OPAQUE else PixelFormat.TRANSLUCENT
|
||||
|
||||
override fun getAlpha(): Int = alpha
|
||||
|
||||
override fun onTimeUpdate(animation: TimeAnimator?, totalTime: Long, deltaTime: Long) {
|
||||
if (callback != null) {
|
||||
callback?.also {
|
||||
updateColor()
|
||||
invalidateSelf()
|
||||
}
|
||||
it.invalidateDrawable(this)
|
||||
} ?: stop()
|
||||
}
|
||||
|
||||
override fun start() {
|
||||
@@ -64,7 +62,7 @@ class AnimatedPlaceholderDrawable(context: Context) : Drawable(), Animatable, Ti
|
||||
}
|
||||
|
||||
override fun stop() {
|
||||
timeAnimator.cancel()
|
||||
timeAnimator.end()
|
||||
}
|
||||
|
||||
override fun isRunning(): Boolean = timeAnimator.isStarted
|
||||
|
||||
@@ -6,7 +6,7 @@ import android.view.ViewGroup
|
||||
import android.widget.ImageView
|
||||
import coil.size.Dimension
|
||||
import coil.size.Size
|
||||
import coil.size.SizeResolver
|
||||
import coil.size.ViewSizeResolver
|
||||
import kotlinx.coroutines.CancellableContinuation
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import kotlin.coroutines.resume
|
||||
@@ -16,24 +16,24 @@ private const val ASPECT_RATIO_HEIGHT = 18f
|
||||
private const val ASPECT_RATIO_WIDTH = 13f
|
||||
|
||||
class CoverSizeResolver(
|
||||
private val imageView: ImageView,
|
||||
) : SizeResolver {
|
||||
override val view: ImageView,
|
||||
) : ViewSizeResolver<ImageView> {
|
||||
|
||||
override suspend fun size(): Size {
|
||||
getSize()?.let { return it }
|
||||
return suspendCancellableCoroutine { cont ->
|
||||
val layoutListener = LayoutListener(cont)
|
||||
imageView.addOnLayoutChangeListener(layoutListener)
|
||||
view.addOnLayoutChangeListener(layoutListener)
|
||||
cont.invokeOnCancellation {
|
||||
imageView.removeOnLayoutChangeListener(layoutListener)
|
||||
view.removeOnLayoutChangeListener(layoutListener)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getSize(): Size? {
|
||||
val lp = imageView.layoutParams
|
||||
var width = getDimension(lp.width, imageView.width, imageView.paddingLeft + imageView.paddingRight)
|
||||
var height = getDimension(lp.height, imageView.height, imageView.paddingTop + imageView.paddingBottom)
|
||||
val lp = view.layoutParams
|
||||
var width = getDimension(lp.width, view.width, view.paddingLeft + view.paddingRight)
|
||||
var height = getDimension(lp.height, view.height, view.paddingTop + view.paddingBottom)
|
||||
if (width == null && height == null) {
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
package org.koitharu.kotatsu.core.ui.util
|
||||
|
||||
import android.view.View
|
||||
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
||||
|
||||
class BottomSheetNoHalfExpandedCallback() : BottomSheetBehavior.BottomSheetCallback() {
|
||||
|
||||
private var previousStableState = BottomSheetBehavior.STATE_COLLAPSED
|
||||
|
||||
override fun onStateChanged(sheet: View, state: Int) {
|
||||
if (state == BottomSheetBehavior.STATE_HALF_EXPANDED) {
|
||||
val behavior = (sheet.layoutParams as? CoordinatorLayout.LayoutParams)?.behavior as? BottomSheetBehavior<*>
|
||||
behavior?.state = previousStableState
|
||||
} else if (state == BottomSheetBehavior.STATE_EXPANDED || state == BottomSheetBehavior.STATE_COLLAPSED) {
|
||||
previousStableState = state
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSlide(sheet: View, offset: Float) = Unit
|
||||
}
|
||||
@@ -54,6 +54,7 @@ import org.koitharu.kotatsu.core.ui.image.ChipIconTarget
|
||||
import org.koitharu.kotatsu.core.ui.image.CoverSizeResolver
|
||||
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
|
||||
import org.koitharu.kotatsu.core.ui.util.BottomSheetClollapseCallback
|
||||
import org.koitharu.kotatsu.core.ui.util.BottomSheetNoHalfExpandedCallback
|
||||
import org.koitharu.kotatsu.core.ui.util.MenuInvalidator
|
||||
import org.koitharu.kotatsu.core.ui.util.ReversibleActionObserver
|
||||
import org.koitharu.kotatsu.core.ui.widgets.ChipsView
|
||||
@@ -155,6 +156,7 @@ class DetailsActivity :
|
||||
viewBinding.chipsTags.onChipClickListener = this
|
||||
TitleScrollCoordinator(viewBinding.textViewTitle).attach(viewBinding.scrollView)
|
||||
viewBinding.containerBottomSheet?.let { BottomSheetBehavior.from(it) }?.let { behavior ->
|
||||
behavior.addBottomSheetCallback(BottomSheetNoHalfExpandedCallback())
|
||||
onBackPressedDispatcher.addCallback(BottomSheetClollapseCallback(behavior))
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import coil.decode.ImageSource
|
||||
import coil.fetch.FetchResult
|
||||
import coil.fetch.Fetcher
|
||||
import coil.fetch.SourceResult
|
||||
import coil.network.HttpException
|
||||
import coil.request.Options
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -44,15 +45,17 @@ class MangaPageFetcher(
|
||||
override suspend fun fetch(): FetchResult {
|
||||
val repo = mangaRepositoryFactory.create(page.source)
|
||||
val pageUrl = repo.getPageUrl(page)
|
||||
pagesCache.get(pageUrl)?.let { file ->
|
||||
return SourceResult(
|
||||
source = ImageSource(
|
||||
file = file.toOkioPath(),
|
||||
metadata = MangaPageMetadata(page),
|
||||
),
|
||||
mimeType = null,
|
||||
dataSource = DataSource.DISK,
|
||||
)
|
||||
if (options.diskCachePolicy.readEnabled) {
|
||||
pagesCache.get(pageUrl)?.let { file ->
|
||||
return SourceResult(
|
||||
source = ImageSource(
|
||||
file = file.toOkioPath(),
|
||||
metadata = MangaPageMetadata(page),
|
||||
),
|
||||
mimeType = null,
|
||||
dataSource = DataSource.DISK,
|
||||
)
|
||||
}
|
||||
}
|
||||
return loadPage(pageUrl)
|
||||
}
|
||||
@@ -91,8 +94,8 @@ class MangaPageFetcher(
|
||||
else -> {
|
||||
val request = PageLoader.createPageRequest(page, pageUrl)
|
||||
imageProxyInterceptor.interceptPageRequest(request, okHttpClient).use { response ->
|
||||
check(response.isSuccessful) {
|
||||
"Invalid response: ${response.code} ${response.message} at $pageUrl"
|
||||
if (!response.isSuccessful) {
|
||||
throw HttpException(response)
|
||||
}
|
||||
val body = checkNotNull(response.body) {
|
||||
"Null response"
|
||||
@@ -122,17 +125,15 @@ class MangaPageFetcher(
|
||||
private val imageProxyInterceptor: ImageProxyInterceptor,
|
||||
) : Fetcher.Factory<MangaPage> {
|
||||
|
||||
override fun create(data: MangaPage, options: Options, imageLoader: ImageLoader): Fetcher {
|
||||
return MangaPageFetcher(
|
||||
okHttpClient = okHttpClient,
|
||||
pagesCache = pagesCache,
|
||||
options = options,
|
||||
page = data,
|
||||
context = context,
|
||||
mangaRepositoryFactory = mangaRepositoryFactory,
|
||||
imageProxyInterceptor = imageProxyInterceptor,
|
||||
)
|
||||
}
|
||||
override fun create(data: MangaPage, options: Options, imageLoader: ImageLoader) = MangaPageFetcher(
|
||||
okHttpClient = okHttpClient,
|
||||
pagesCache = pagesCache,
|
||||
options = options,
|
||||
page = data,
|
||||
context = context,
|
||||
mangaRepositoryFactory = mangaRepositoryFactory,
|
||||
imageProxyInterceptor = imageProxyInterceptor,
|
||||
)
|
||||
}
|
||||
|
||||
class MangaPageMetadata(val page: MangaPage) : ImageSource.Metadata()
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
package org.koitharu.kotatsu.details.ui.pager.pages
|
||||
|
||||
import coil.key.Keyer
|
||||
import coil.request.Options
|
||||
import org.koitharu.kotatsu.parsers.model.MangaPage
|
||||
|
||||
class MangaPageKeyer : Keyer<MangaPage> {
|
||||
|
||||
override fun key(data: MangaPage, options: Options) = data.url
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<integer name="config_longAnimTime">1000</integer>
|
||||
<integer name="config_defaultAnimTime">300</integer>
|
||||
<integer name="config_shorterAnimTime">150</integer>
|
||||
<integer name="config_tinyAnimTime">50</integer>
|
||||
|
||||
Reference in New Issue
Block a user