Fix images memory caching

master
Koitharu 2 years ago
parent 5ef1b4ac9c
commit 1a60df6d98
Signed by: Koitharu
GPG Key ID: 676DEE768C17A9D7

@ -16,8 +16,8 @@ android {
applicationId 'org.koitharu.kotatsu' applicationId 'org.koitharu.kotatsu'
minSdk = 21 minSdk = 21
targetSdk = 34 targetSdk = 34
versionCode = 636 versionCode = 637
versionName = '7.0-b2' versionName = '7.0-b3'
generatedDensities = [] generatedDensities = []
testInstrumentationRunner 'org.koitharu.kotatsu.HiltTestRunner' testInstrumentationRunner 'org.koitharu.kotatsu.HiltTestRunner'
ksp { 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.connectivityManager
import org.koitharu.kotatsu.core.util.ext.isLowRamDevice 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.local.data.CacheDir import org.koitharu.kotatsu.local.data.CacheDir
import org.koitharu.kotatsu.local.data.CbzFetcher import org.koitharu.kotatsu.local.data.CbzFetcher
import org.koitharu.kotatsu.local.data.LocalStorageChanges import org.koitharu.kotatsu.local.data.LocalStorageChanges
@ -101,8 +102,8 @@ interface AppModule {
return ImageLoader.Builder(context) return ImageLoader.Builder(context)
.okHttpClient(okHttpClient.newBuilder().cache(null).build()) .okHttpClient(okHttpClient.newBuilder().cache(null).build())
.interceptorDispatcher(Dispatchers.Default) .interceptorDispatcher(Dispatchers.Default)
.fetcherDispatcher(Dispatchers.IO) .fetcherDispatcher(Dispatchers.Default)
.decoderDispatcher(Dispatchers.Default) .decoderDispatcher(Dispatchers.IO)
.transformationDispatcher(Dispatchers.Default) .transformationDispatcher(Dispatchers.Default)
.diskCache(diskCacheFactory) .diskCache(diskCacheFactory)
.logger(if (BuildConfig.DEBUG) DebugLogger() else null) .logger(if (BuildConfig.DEBUG) DebugLogger() else null)
@ -113,6 +114,7 @@ interface AppModule {
.add(SvgDecoder.Factory()) .add(SvgDecoder.Factory())
.add(CbzFetcher.Factory()) .add(CbzFetcher.Factory())
.add(FaviconFetcher.Factory(context, okHttpClient, mangaRepositoryFactory)) .add(FaviconFetcher.Factory(context, okHttpClient, mangaRepositoryFactory))
.add(MangaPageKeyer())
.add(pageFetcherFactory) .add(pageFetcherFactory)
.add(imageProxyInterceptor) .add(imageProxyInterceptor)
.add(coverRestoreInterceptor) .add(coverRestoreInterceptor)

@ -7,23 +7,23 @@ import android.graphics.ColorFilter
import android.graphics.PixelFormat import android.graphics.PixelFormat
import android.graphics.drawable.Animatable import android.graphics.drawable.Animatable
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
import android.view.animation.AccelerateDecelerateInterpolator
import androidx.core.graphics.ColorUtils import androidx.core.graphics.ColorUtils
import androidx.interpolator.view.animation.FastOutSlowInInterpolator import androidx.interpolator.view.animation.FastOutSlowInInterpolator
import com.google.android.material.animation.ArgbEvaluatorCompat 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 org.koitharu.kotatsu.core.util.ext.getThemeColor
import kotlin.math.abs import kotlin.math.abs
import com.google.android.material.R as materialR import com.google.android.material.R as materialR
class AnimatedPlaceholderDrawable(context: Context) : Drawable(), Animatable, TimeAnimator.TimeListener { class AnimatedPlaceholderDrawable(context: Context) : Drawable(), Animatable, TimeAnimator.TimeListener {
private val colorLow = context.getThemeColor(materialR.attr.colorBackgroundFloating) private val colorLow = context.getThemeColor(materialR.attr.colorSurfaceContainerLowest)
private val colorHigh = context.getThemeColor(materialR.attr.colorSurfaceContainer) private val colorHigh = context.getThemeColor(materialR.attr.colorSurfaceContainerHighest)
private var currentColor: Int = colorLow private var currentColor: Int = colorLow
private var alpha: Int = 255 private var alpha: Int = 255
private val interpolator = FastOutSlowInInterpolator() private val interpolator = FastOutSlowInInterpolator()
private val period = 2000 * context.animatorDurationScale private val period = context.getAnimationDuration(R.integer.config_longAnimTime) * 2
private val timeAnimator = TimeAnimator() private val timeAnimator = TimeAnimator()
init { init {
@ -40,23 +40,21 @@ class AnimatedPlaceholderDrawable(context: Context) : Drawable(), Animatable, Ti
} }
override fun setAlpha(alpha: Int) { override fun setAlpha(alpha: Int) {
this.alpha = alpha // this.alpha = alpha FIXME coil's crossfade
} }
override fun setColorFilter(colorFilter: ColorFilter?) { override fun setColorFilter(colorFilter: ColorFilter?) = Unit
throw UnsupportedOperationException("ColorFilter is not supported by PlaceholderDrawable")
}
@Deprecated("Deprecated in Java") @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 getAlpha(): Int = alpha
override fun onTimeUpdate(animation: TimeAnimator?, totalTime: Long, deltaTime: Long) { override fun onTimeUpdate(animation: TimeAnimator?, totalTime: Long, deltaTime: Long) {
if (callback != null) { callback?.also {
updateColor() updateColor()
invalidateSelf() it.invalidateDrawable(this)
} } ?: stop()
} }
override fun start() { override fun start() {
@ -64,7 +62,7 @@ class AnimatedPlaceholderDrawable(context: Context) : Drawable(), Animatable, Ti
} }
override fun stop() { override fun stop() {
timeAnimator.cancel() timeAnimator.end()
} }
override fun isRunning(): Boolean = timeAnimator.isStarted override fun isRunning(): Boolean = timeAnimator.isStarted

@ -6,7 +6,7 @@ import android.view.ViewGroup
import android.widget.ImageView import android.widget.ImageView
import coil.size.Dimension import coil.size.Dimension
import coil.size.Size import coil.size.Size
import coil.size.SizeResolver import coil.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
@ -16,24 +16,24 @@ private const val ASPECT_RATIO_HEIGHT = 18f
private const val ASPECT_RATIO_WIDTH = 13f private const val ASPECT_RATIO_WIDTH = 13f
class CoverSizeResolver( class CoverSizeResolver(
private val imageView: ImageView, override val view: ImageView,
) : SizeResolver { ) : ViewSizeResolver<ImageView> {
override suspend fun size(): Size { override suspend fun size(): Size {
getSize()?.let { return it } getSize()?.let { return it }
return suspendCancellableCoroutine { cont -> return suspendCancellableCoroutine { cont ->
val layoutListener = LayoutListener(cont) val layoutListener = LayoutListener(cont)
imageView.addOnLayoutChangeListener(layoutListener) view.addOnLayoutChangeListener(layoutListener)
cont.invokeOnCancellation { cont.invokeOnCancellation {
imageView.removeOnLayoutChangeListener(layoutListener) view.removeOnLayoutChangeListener(layoutListener)
} }
} }
} }
private fun getSize(): Size? { private fun getSize(): Size? {
val lp = imageView.layoutParams val lp = view.layoutParams
var width = getDimension(lp.width, imageView.width, imageView.paddingLeft + imageView.paddingRight) var width = getDimension(lp.width, view.width, view.paddingLeft + view.paddingRight)
var height = getDimension(lp.height, imageView.height, imageView.paddingTop + imageView.paddingBottom) var height = getDimension(lp.height, view.height, view.paddingTop + view.paddingBottom)
if (width == null && height == null) { if (width == null && height == null) {
return 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.image.CoverSizeResolver
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.ui.util.BottomSheetClollapseCallback 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.MenuInvalidator
import org.koitharu.kotatsu.core.ui.util.ReversibleActionObserver import org.koitharu.kotatsu.core.ui.util.ReversibleActionObserver
import org.koitharu.kotatsu.core.ui.widgets.ChipsView import org.koitharu.kotatsu.core.ui.widgets.ChipsView
@ -155,6 +156,7 @@ class DetailsActivity :
viewBinding.chipsTags.onChipClickListener = this viewBinding.chipsTags.onChipClickListener = this
TitleScrollCoordinator(viewBinding.textViewTitle).attach(viewBinding.scrollView) TitleScrollCoordinator(viewBinding.textViewTitle).attach(viewBinding.scrollView)
viewBinding.containerBottomSheet?.let { BottomSheetBehavior.from(it) }?.let { behavior -> viewBinding.containerBottomSheet?.let { BottomSheetBehavior.from(it) }?.let { behavior ->
behavior.addBottomSheetCallback(BottomSheetNoHalfExpandedCallback())
onBackPressedDispatcher.addCallback(BottomSheetClollapseCallback(behavior)) onBackPressedDispatcher.addCallback(BottomSheetClollapseCallback(behavior))
} }

@ -10,6 +10,7 @@ import coil.decode.ImageSource
import coil.fetch.FetchResult import coil.fetch.FetchResult
import coil.fetch.Fetcher import coil.fetch.Fetcher
import coil.fetch.SourceResult import coil.fetch.SourceResult
import coil.network.HttpException
import coil.request.Options import coil.request.Options
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@ -44,15 +45,17 @@ class MangaPageFetcher(
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)
pagesCache.get(pageUrl)?.let { file -> if (options.diskCachePolicy.readEnabled) {
return SourceResult( pagesCache.get(pageUrl)?.let { file ->
source = ImageSource( return SourceResult(
file = file.toOkioPath(), source = ImageSource(
metadata = MangaPageMetadata(page), file = file.toOkioPath(),
), metadata = MangaPageMetadata(page),
mimeType = null, ),
dataSource = DataSource.DISK, mimeType = null,
) dataSource = DataSource.DISK,
)
}
} }
return loadPage(pageUrl) return loadPage(pageUrl)
} }
@ -91,8 +94,8 @@ class MangaPageFetcher(
else -> { else -> {
val request = PageLoader.createPageRequest(page, pageUrl) val request = PageLoader.createPageRequest(page, pageUrl)
imageProxyInterceptor.interceptPageRequest(request, okHttpClient).use { response -> imageProxyInterceptor.interceptPageRequest(request, okHttpClient).use { response ->
check(response.isSuccessful) { if (!response.isSuccessful) {
"Invalid response: ${response.code} ${response.message} at $pageUrl" throw HttpException(response)
} }
val body = checkNotNull(response.body) { val body = checkNotNull(response.body) {
"Null response" "Null response"
@ -122,17 +125,15 @@ class MangaPageFetcher(
private val imageProxyInterceptor: ImageProxyInterceptor, private val imageProxyInterceptor: ImageProxyInterceptor,
) : Fetcher.Factory<MangaPage> { ) : Fetcher.Factory<MangaPage> {
override fun create(data: MangaPage, options: Options, imageLoader: ImageLoader): Fetcher { override fun create(data: MangaPage, options: Options, imageLoader: ImageLoader) = MangaPageFetcher(
return MangaPageFetcher( okHttpClient = okHttpClient,
okHttpClient = okHttpClient, pagesCache = pagesCache,
pagesCache = pagesCache, options = options,
options = options, page = data,
page = data, context = context,
context = context, mangaRepositoryFactory = mangaRepositoryFactory,
mangaRepositoryFactory = mangaRepositoryFactory, imageProxyInterceptor = imageProxyInterceptor,
imageProxyInterceptor = imageProxyInterceptor, )
)
}
} }
class MangaPageMetadata(val page: MangaPage) : ImageSource.Metadata() 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"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<integer name="config_longAnimTime">1000</integer>
<integer name="config_defaultAnimTime">300</integer> <integer name="config_defaultAnimTime">300</integer>
<integer name="config_shorterAnimTime">150</integer> <integer name="config_shorterAnimTime">150</integer>
<integer name="config_tinyAnimTime">50</integer> <integer name="config_tinyAnimTime">50</integer>

Loading…
Cancel
Save