diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/image/CoverSizeResolver.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/image/CoverSizeResolver.kt index d90134da7..588543142 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/image/CoverSizeResolver.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/image/CoverSizeResolver.kt @@ -1,13 +1,12 @@ package org.koitharu.kotatsu.core.ui.image -import android.view.View -import android.view.View.OnLayoutChangeListener import android.view.ViewGroup +import android.view.ViewTreeObserver +import android.view.ViewTreeObserver.OnPreDrawListener import android.widget.ImageView import coil3.size.Dimension import coil3.size.Size import coil3.size.ViewSizeResolver -import kotlinx.coroutines.CancellableContinuation import kotlinx.coroutines.suspendCancellableCoroutine import kotlin.coroutines.resume import kotlin.math.roundToInt @@ -20,31 +19,67 @@ class CoverSizeResolver( ) : ViewSizeResolver { override suspend fun size(): Size { + // Fast path: the view is already measured. getSize()?.let { return it } - return suspendCancellableCoroutine { cont -> - val layoutListener = LayoutListener(cont) - view.addOnLayoutChangeListener(layoutListener) - cont.invokeOnCancellation { - view.removeOnLayoutChangeListener(layoutListener) + + // Slow path: wait for the view to be measured. + return suspendCancellableCoroutine { continuation -> + val viewTreeObserver = view.viewTreeObserver + + val preDrawListener = object : OnPreDrawListener { + private var isResumed = false + + override fun onPreDraw(): Boolean { + val size = getSize() + if (size != null) { + viewTreeObserver.removePreDrawListenerSafe(this) + + if (!isResumed) { + isResumed = true + continuation.resume(size) + } + } + return true + } + } + + viewTreeObserver.addOnPreDrawListener(preDrawListener) + + continuation.invokeOnCancellation { + viewTreeObserver.removePreDrawListenerSafe(preDrawListener) } } } private fun getSize(): Size? { - 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 - } - if (height == null && width != null) { - height = Dimension((width.px * ASPECT_RATIO_HEIGHT / ASPECT_RATIO_WIDTH).roundToInt()) - } else if (width == null && height != null) { - width = Dimension((height.px * ASPECT_RATIO_WIDTH / ASPECT_RATIO_HEIGHT).roundToInt()) + var width = getWidth() + var height = getHeight() + when { + width == null && height == null -> { + return null + } + height == null && width != null -> { + height = Dimension((width.px * ASPECT_RATIO_HEIGHT / ASPECT_RATIO_WIDTH).roundToInt()) + } + width == null && height != null -> { + width = Dimension((height.px * ASPECT_RATIO_WIDTH / ASPECT_RATIO_HEIGHT).roundToInt()) + } } return Size(checkNotNull(width), checkNotNull(height)) } + private fun getWidth() = getDimension( + paramSize = view.layoutParams?.width ?: -1, + viewSize = view.width, + paddingSize = if (subtractPadding) view.paddingLeft + view.paddingRight else 0 + ) + + private fun getHeight() = getDimension( + paramSize = view.layoutParams?.height ?: -1, + viewSize = view.height, + paddingSize = if (subtractPadding) view.paddingTop + view.paddingBottom else 0 + ) + private fun getDimension(paramSize: Int, viewSize: Int, paddingSize: Int): Dimension.Pixels? { if (paramSize == ViewGroup.LayoutParams.WRAP_CONTENT) { return null @@ -60,24 +95,11 @@ class CoverSizeResolver( return null } - private inner class LayoutListener( - private val continuation: CancellableContinuation, - ) : OnLayoutChangeListener { - - override fun onLayoutChange( - v: View, - left: Int, - top: Int, - right: Int, - bottom: Int, - oldLeft: Int, - oldTop: Int, - oldRight: Int, - oldBottom: Int, - ) { - val size = getSize() ?: return - v.removeOnLayoutChangeListener(this) - continuation.resume(size) + private fun ViewTreeObserver.removePreDrawListenerSafe(victim: OnPreDrawListener) { + if (isAlive) { + removeOnPreDrawListener(victim) + } else { + view.viewTreeObserver.removeOnPreDrawListener(victim) } } } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/DetailsActivity.kt b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/DetailsActivity.kt index 51ef4d467..a52f757e3 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/DetailsActivity.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/DetailsActivity.kt @@ -446,7 +446,7 @@ class DetailsActivity : val manga = details.toManga() with(viewBinding) { textViewTitle.text = manga.title - textViewSubtitle.textAndVisible = manga.altTitle + textViewSubtitle.textAndVisible = manga.altTitles.joinToString("\n") textViewNsfw.isVisible = manga.isNsfw textViewDescription.text = details.description.ifNullOrEmpty { getString(R.string.no_description) } } @@ -561,6 +561,7 @@ class DetailsActivity : } private fun loadCover(imageUrl: String?) { + viewBinding.imageViewCover.isEnabled = !imageUrl.isNullOrEmpty() val lastResult = CoilUtils.result(viewBinding.imageViewCover) if (lastResult is SuccessResult && lastResult.request.data == imageUrl) { return diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/settings/userdata/storage/StorageManageSettingsViewModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/settings/userdata/storage/StorageManageSettingsViewModel.kt index 6ab399c35..db5f2b87c 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/settings/userdata/storage/StorageManageSettingsViewModel.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/settings/userdata/storage/StorageManageSettingsViewModel.kt @@ -1,5 +1,6 @@ package org.koitharu.kotatsu.settings.userdata.storage +import coil3.ImageLoader import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job @@ -35,6 +36,7 @@ class StorageManageSettingsViewModel @Inject constructor( private val cookieJar: MutableCookieJar, private val deleteReadChaptersUseCase: DeleteReadChaptersUseCase, private val mangaDataRepositoryProvider: Provider, + private val coil: ImageLoader, ) : BaseViewModel() { val onActionDone = MutableEventFlow() @@ -78,6 +80,9 @@ class StorageManageSettingsViewModel @Inject constructor( storageManager.clearCache(cache) checkNotNull(cacheSizes[cache]).value = storageManager.computeCacheSize(cache) loadStorageUsage() + if (cache == CacheDir.THUMBS || cache == CacheDir.FAVICONS) { + coil.memoryCache?.clear() + } } finally { loadingKeys.update { it - key } } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index f396f9500..113e82445 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,6 +1,6 @@ [versions] acra = "5.12.0" -activity = "1.10.0" +activity = "1.10.1" adapterdelegates = "4.3.2" appcompat = "1.7.0" avifDecoder = "1.1.1.14d8e3c4" @@ -9,10 +9,10 @@ coil = "3.1.0" collections = "1.4.5" #noinspection GradleDependency - 2.5.3 cause crashes conscrypt = "2.5.2" -constraintlayout = "2.2.0" +constraintlayout = "2.2.1" coreKtx = "1.15.0" coroutines = "1.10.1" -desugar = "2.1.4" +desugar = "2.1.5" diskLruCache = "1.4" fragment = "1.8.6" gradle = "8.8.1" @@ -27,11 +27,11 @@ ksp = "2.1.10-1.0.29" leakcanary = "3.0-alpha-8" lifecycle = "2.8.7" markwon = "4.6.2" -material = "1.13.0-alpha10" +material = "1.13.0-alpha11" moshi = "1.15.2" okhttp = "4.12.0" okio = "3.10.2" -parsers = "ddb9b13df7" +parsers = "843d1f1bea" preference = "1.2.1" recyclerview = "1.4.0" room = "2.6.1"