From fcdfaf556430351bce1424fbab5ce2abe0bd6ac4 Mon Sep 17 00:00:00 2001 From: Koitharu Date: Mon, 8 Aug 2022 10:49:03 +0300 Subject: [PATCH 1/4] Fix covers size resolving --- .../kotatsu/details/ui/DetailsFragment.kt | 16 +++- .../list/ui/adapter/MangaGridItemAD.kt | 5 +- .../ui/adapter/MangaListDetailedItemAD.kt | 7 +- .../list/ui/adapter/MangaListItemAD.kt | 5 +- .../kotatsu/utils/image/CoverSizeResolver.kt | 83 +++++++++++++++++++ app/src/main/res/layout/item_feed.xml | 3 +- app/src/main/res/layout/item_manga_list.xml | 3 +- 7 files changed, 107 insertions(+), 15 deletions(-) create mode 100644 app/src/main/java/org/koitharu/kotatsu/utils/image/CoverSizeResolver.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsFragment.kt b/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsFragment.kt index 21c6541ea..677d7e3f1 100644 --- a/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsFragment.kt +++ b/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsFragment.kt @@ -46,6 +46,7 @@ import org.koitharu.kotatsu.search.ui.SearchActivity import org.koitharu.kotatsu.utils.FileSize import org.koitharu.kotatsu.utils.ShareHelper import org.koitharu.kotatsu.utils.ext.* +import org.koitharu.kotatsu.utils.image.CoverSizeResolver class DetailsFragment : BaseFragment(), @@ -354,13 +355,22 @@ class DetailsFragment : } val request = ImageRequest.Builder(context ?: return) .target(binding.imageViewCover) + .size(CoverSizeResolver(binding.imageViewCover)) .data(imageUrl) .crossfade(true) .referer(manga.publicUrl) .lifecycle(viewLifecycleOwner) - lastResult?.drawable?.let { - request.fallback(it) - } ?: request.fallback(R.drawable.ic_placeholder) + .placeholderMemoryCacheKey(manga.coverUrl) + val previousDrawable = lastResult?.drawable + if (previousDrawable != null) { + request.fallback(previousDrawable) + .placeholder(previousDrawable) + .error(previousDrawable) + } else { + request.fallback(R.drawable.ic_placeholder) + .placeholder(R.drawable.ic_placeholder) + .error(R.drawable.ic_placeholder) + } request.enqueueWith(coil) } diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/MangaGridItemAD.kt b/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/MangaGridItemAD.kt index 82b77d11f..6af70140e 100644 --- a/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/MangaGridItemAD.kt +++ b/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/MangaGridItemAD.kt @@ -17,6 +17,7 @@ import org.koitharu.kotatsu.utils.ext.disposeImageRequest import org.koitharu.kotatsu.utils.ext.enqueueWith import org.koitharu.kotatsu.utils.ext.newImageRequest import org.koitharu.kotatsu.utils.ext.referer +import org.koitharu.kotatsu.utils.image.CoverSizeResolver fun mangaGridItemAD( coil: ImageLoader, @@ -24,9 +25,8 @@ fun mangaGridItemAD( clickListener: OnListItemClickListener, sizeResolver: ItemSizeResolver?, ) = adapterDelegateViewBinding( - { inflater, parent -> ItemMangaGridBinding.inflate(inflater, parent, false) } + { inflater, parent -> ItemMangaGridBinding.inflate(inflater, parent, false) }, ) { - var badge: BadgeDrawable? = null itemView.setOnClickListener { @@ -46,6 +46,7 @@ fun mangaGridItemAD( binding.progressView.setPercent(item.progress, MangaListAdapter.PAYLOAD_PROGRESS in payloads) binding.imageViewCover.newImageRequest(item.coverUrl)?.run { referer(item.manga.publicUrl) + size(CoverSizeResolver(binding.imageViewCover)) placeholder(R.drawable.ic_placeholder) fallback(R.drawable.ic_placeholder) error(R.drawable.ic_placeholder) diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/MangaListDetailedItemAD.kt b/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/MangaListDetailedItemAD.kt index edf38d33a..4c1443b72 100644 --- a/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/MangaListDetailedItemAD.kt +++ b/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/MangaListDetailedItemAD.kt @@ -2,7 +2,6 @@ package org.koitharu.kotatsu.list.ui.adapter import androidx.lifecycle.LifecycleOwner import coil.ImageLoader -import org.koitharu.kotatsu.utils.ext.* import com.google.android.material.badge.BadgeDrawable import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding import org.koitharu.kotatsu.R @@ -12,15 +11,16 @@ import org.koitharu.kotatsu.history.domain.PROGRESS_NONE import org.koitharu.kotatsu.list.ui.model.ListModel import org.koitharu.kotatsu.list.ui.model.MangaListDetailedModel import org.koitharu.kotatsu.parsers.model.Manga +import org.koitharu.kotatsu.utils.ext.* +import org.koitharu.kotatsu.utils.image.CoverSizeResolver fun mangaListDetailedItemAD( coil: ImageLoader, lifecycleOwner: LifecycleOwner, clickListener: OnListItemClickListener, ) = adapterDelegateViewBinding( - { inflater, parent -> ItemMangaListDetailsBinding.inflate(inflater, parent, false) } + { inflater, parent -> ItemMangaListDetailsBinding.inflate(inflater, parent, false) }, ) { - var badge: BadgeDrawable? = null itemView.setOnClickListener { @@ -36,6 +36,7 @@ fun mangaListDetailedItemAD( binding.progressView.setPercent(item.progress, MangaListAdapter.PAYLOAD_PROGRESS in payloads) binding.imageViewCover.newImageRequest(item.coverUrl)?.run { referer(item.manga.publicUrl) + size(CoverSizeResolver(binding.imageViewCover)) placeholder(R.drawable.ic_placeholder) fallback(R.drawable.ic_placeholder) error(R.drawable.ic_placeholder) diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/MangaListItemAD.kt b/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/MangaListItemAD.kt index 48c157ed1..fb8f5e216 100644 --- a/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/MangaListItemAD.kt +++ b/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/MangaListItemAD.kt @@ -2,7 +2,6 @@ package org.koitharu.kotatsu.list.ui.adapter import androidx.lifecycle.LifecycleOwner import coil.ImageLoader -import org.koitharu.kotatsu.utils.ext.* import com.google.android.material.badge.BadgeDrawable import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding import org.koitharu.kotatsu.R @@ -11,15 +10,15 @@ import org.koitharu.kotatsu.databinding.ItemMangaListBinding import org.koitharu.kotatsu.list.ui.model.ListModel import org.koitharu.kotatsu.list.ui.model.MangaListModel import org.koitharu.kotatsu.parsers.model.Manga +import org.koitharu.kotatsu.utils.ext.* fun mangaListItemAD( coil: ImageLoader, lifecycleOwner: LifecycleOwner, clickListener: OnListItemClickListener, ) = adapterDelegateViewBinding( - { inflater, parent -> ItemMangaListBinding.inflate(inflater, parent, false) } + { inflater, parent -> ItemMangaListBinding.inflate(inflater, parent, false) }, ) { - var badge: BadgeDrawable? = null itemView.setOnClickListener { diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/image/CoverSizeResolver.kt b/app/src/main/java/org/koitharu/kotatsu/utils/image/CoverSizeResolver.kt new file mode 100644 index 000000000..69f61133f --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/utils/image/CoverSizeResolver.kt @@ -0,0 +1,83 @@ +package org.koitharu.kotatsu.utils.image + +import android.view.View +import android.view.View.OnLayoutChangeListener +import android.view.ViewGroup +import android.widget.ImageView +import coil.size.Dimension +import coil.size.Size +import coil.size.SizeResolver +import kotlin.coroutines.resume +import kotlin.math.roundToInt +import kotlinx.coroutines.CancellableContinuation +import kotlinx.coroutines.suspendCancellableCoroutine + +private const val ASPECT_RATIO_HEIGHT = 18f +private const val ASPECT_RATIO_WIDTH = 13f + +class CoverSizeResolver( + private val imageView: ImageView, +) : SizeResolver { + + override suspend fun size(): Size { + getSize()?.let { return it } + return suspendCancellableCoroutine { cont -> + val layoutListener = LayoutListener(cont) + imageView.addOnLayoutChangeListener(layoutListener) + cont.invokeOnCancellation { + imageView.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) + 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()) + } + return Size(checkNotNull(width), checkNotNull(height)) + } + + private fun getDimension(paramSize: Int, viewSize: Int, paddingSize: Int): Dimension.Pixels? { + if (paramSize == ViewGroup.LayoutParams.WRAP_CONTENT) { + return null + } + val insetParamSize = paramSize - paddingSize + if (insetParamSize > 0) { + return Dimension(insetParamSize) + } + val insetViewSize = viewSize - paddingSize + if (insetViewSize > 0) { + return Dimension(insetViewSize) + } + 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) + } + } +} \ No newline at end of file diff --git a/app/src/main/res/layout/item_feed.xml b/app/src/main/res/layout/item_feed.xml index 1dcb660b8..69a94a29f 100644 --- a/app/src/main/res/layout/item_feed.xml +++ b/app/src/main/res/layout/item_feed.xml @@ -12,10 +12,9 @@ Date: Mon, 8 Aug 2022 11:04:25 +0300 Subject: [PATCH 2/4] Failsafe implementation of MangaSource.valueOf --- .../org/koitharu/kotatsu/core/model/MangaSource.kt | 10 +++++++++- .../org/koitharu/kotatsu/core/parser/FaviconMapper.kt | 4 ++-- .../kotatsu/settings/onboard/OnboardViewModel.kt | 9 +++++---- 3 files changed, 16 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/org/koitharu/kotatsu/core/model/MangaSource.kt b/app/src/main/java/org/koitharu/kotatsu/core/model/MangaSource.kt index 9bd4ef5cf..22c2319bf 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/model/MangaSource.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/model/MangaSource.kt @@ -1,10 +1,18 @@ package org.koitharu.kotatsu.core.model +import java.util.* import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.parsers.util.toTitleCase -import java.util.* fun MangaSource.getLocaleTitle(): String? { val lc = Locale(locale ?: return null) return lc.getDisplayLanguage(lc).toTitleCase(lc) +} + +@Suppress("FunctionName") +fun MangaSource(name: String): MangaSource? { + MangaSource.values().forEach { + if (it.name == name) return it + } + return null } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/core/parser/FaviconMapper.kt b/app/src/main/java/org/koitharu/kotatsu/core/parser/FaviconMapper.kt index ba5412a50..0f994ecaa 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/parser/FaviconMapper.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/parser/FaviconMapper.kt @@ -5,7 +5,7 @@ import coil.map.Mapper import coil.request.Options import okhttp3.HttpUrl import okhttp3.HttpUrl.Companion.toHttpUrl -import org.koitharu.kotatsu.parsers.model.MangaSource +import org.koitharu.kotatsu.core.model.MangaSource class FaviconMapper : Mapper { @@ -13,7 +13,7 @@ class FaviconMapper : Mapper { if (data.scheme != "favicon") { return null } - val mangaSource = MangaSource.valueOf(data.schemeSpecificPart) + val mangaSource = MangaSource(data.schemeSpecificPart) ?: return null val repo = MangaRepository(mangaSource) as RemoteMangaRepository return repo.getFaviconUrl().toHttpUrl() } diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/onboard/OnboardViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/settings/onboard/OnboardViewModel.kt index 2f1495243..05d7f0906 100644 --- a/app/src/main/java/org/koitharu/kotatsu/settings/onboard/OnboardViewModel.kt +++ b/app/src/main/java/org/koitharu/kotatsu/settings/onboard/OnboardViewModel.kt @@ -3,15 +3,16 @@ package org.koitharu.kotatsu.settings.onboard import androidx.collection.ArraySet import androidx.core.os.LocaleListCompat import androidx.lifecycle.MutableLiveData +import java.util.* import org.koitharu.kotatsu.base.ui.BaseViewModel +import org.koitharu.kotatsu.core.model.MangaSource import org.koitharu.kotatsu.core.prefs.AppSettings -import org.koitharu.kotatsu.parsers.model.MangaSource +import org.koitharu.kotatsu.parsers.util.mapNotNullToSet import org.koitharu.kotatsu.parsers.util.mapToSet import org.koitharu.kotatsu.parsers.util.toTitleCase import org.koitharu.kotatsu.settings.onboard.model.SourceLocale import org.koitharu.kotatsu.utils.ext.map import org.koitharu.kotatsu.utils.ext.mapToSet -import java.util.* class OnboardViewModel( private val settings: AppSettings, @@ -27,7 +28,7 @@ class OnboardViewModel( init { if (settings.isSourcesSelected) { - selectedLocales.removeAll(settings.hiddenSources.mapToSet { x -> MangaSource.valueOf(x).locale }) + selectedLocales.removeAll(settings.hiddenSources.mapNotNullToSet { x -> MangaSource(x)?.locale }) } else { val deviceLocales = LocaleListCompat.getDefault().mapToSet { x -> x.language @@ -66,7 +67,7 @@ class OnboardViewModel( SourceLocale( key = key, title = locale?.getDisplayLanguage(locale)?.toTitleCase(locale), - isChecked = key in selectedLocales + isChecked = key in selectedLocales, ) }.sortedWith(SourceLocaleComparator()) } From 436233e7351df04d0f93d78681dd1ae57307b63d Mon Sep 17 00:00:00 2001 From: Koitharu Date: Mon, 8 Aug 2022 11:49:48 +0300 Subject: [PATCH 3/4] Fix description text color --- .../kotatsu/details/ui/DetailsViewModel.kt | 23 +++++++++++++++---- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsViewModel.kt index d77ef9aba..a8bade779 100644 --- a/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsViewModel.kt +++ b/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsViewModel.kt @@ -1,11 +1,16 @@ package org.koitharu.kotatsu.details.ui import android.text.Html +import android.text.SpannableString +import android.text.Spanned +import android.text.style.ForegroundColorSpan +import androidx.core.text.getSpans import androidx.core.text.parseAsHtml import androidx.lifecycle.LiveData import androidx.lifecycle.asFlow import androidx.lifecycle.asLiveData import androidx.lifecycle.viewModelScope +import java.io.IOException import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.flow.* @@ -33,7 +38,6 @@ import org.koitharu.kotatsu.tracker.domain.TrackingRepository import org.koitharu.kotatsu.utils.SingleLiveEvent import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct import org.koitharu.kotatsu.utils.ext.printStackTraceDebug -import java.io.IOException class DetailsViewModel( intent: MangaIntent, @@ -91,8 +95,8 @@ class DetailsViewModel( if (description.isNullOrEmpty()) { emit(null) } else { - emit(description.parseAsHtml()) - emit(description.parseAsHtml(imageGetter = imageGetter)) + emit(description.parseAsHtml().filterSpans()) + emit(description.parseAsHtml(imageGetter = imageGetter).filterSpans()) } }.asLiveDataDistinct(viewModelScope.coroutineContext + Dispatchers.Default, null) @@ -110,7 +114,7 @@ class DetailsViewModel( val selectedBranchIndex = combine( branches.asFlow(), - delegate.selectedBranch + delegate.selectedBranch, ) { branches, selected -> branches.indexOf(selected) }.asLiveDataDistinct(viewModelScope.coroutineContext + Dispatchers.Default) @@ -225,7 +229,7 @@ class DetailsViewModel( fun unregisterScrobbling() { launchJob(Dispatchers.Default) { scrobbler.unregisterScrobbling( - mangaId = delegate.mangaId + mangaId = delegate.mangaId, ) } } @@ -242,4 +246,13 @@ class DetailsViewModel( it.chapter.name.contains(query, ignoreCase = true) } } + + private fun Spanned.filterSpans(): CharSequence { + val spannable = SpannableString.valueOf(this) + val spans = spannable.getSpans() + for (span in spans) { + spannable.removeSpan(span) + } + return spannable.trim() + } } \ No newline at end of file From 87afad29ce94ed6d91fbb2766790798b4c8b6d16 Mon Sep 17 00:00:00 2001 From: Koitharu Date: Mon, 8 Aug 2022 16:11:24 +0300 Subject: [PATCH 4/4] Update parsers --- app/build.gradle | 6 ++--- .../kotatsu/utils/image/TrimTransformation.kt | 23 ++++++++++++++----- 2 files changed, 20 insertions(+), 9 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 09247a335..117ab9138 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -14,8 +14,8 @@ android { applicationId 'org.koitharu.kotatsu' minSdkVersion 21 targetSdkVersion 32 - versionCode 420 - versionName '3.4.8' + versionCode 421 + versionName '3.4.9' generatedDensities = [] testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" @@ -79,7 +79,7 @@ afterEvaluate { } } dependencies { - implementation('com.github.KotatsuApp:kotatsu-parsers:e2308214a7') { + implementation('com.github.KotatsuApp:kotatsu-parsers:8709c3dd0c') { exclude group: 'org.json', module: 'json' } diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/image/TrimTransformation.kt b/app/src/main/java/org/koitharu/kotatsu/utils/image/TrimTransformation.kt index 2640060b7..b44281f38 100644 --- a/app/src/main/java/org/koitharu/kotatsu/utils/image/TrimTransformation.kt +++ b/app/src/main/java/org/koitharu/kotatsu/utils/image/TrimTransformation.kt @@ -1,11 +1,15 @@ package org.koitharu.kotatsu.utils.image import android.graphics.Bitmap -import androidx.core.graphics.get +import androidx.annotation.ColorInt +import androidx.core.graphics.* import coil.size.Size import coil.transform.Transformation +import kotlin.math.abs -class TrimTransformation : Transformation { +class TrimTransformation( + private val tolerance: Int = 20, +) : Transformation { override val cacheKey: String = javaClass.name @@ -20,7 +24,7 @@ class TrimTransformation : Transformation { var isColBlank = true val prevColor = input[x, 0] for (y in 1 until input.height) { - if (input[x, y] != prevColor) { + if (!isColorTheSame(input[x, y], prevColor)) { isColBlank = false break } @@ -39,7 +43,7 @@ class TrimTransformation : Transformation { var isColBlank = true val prevColor = input[x, 0] for (y in 1 until input.height) { - if (input[x, y] != prevColor) { + if (!isColorTheSame(input[x, y], prevColor)) { isColBlank = false break } @@ -55,7 +59,7 @@ class TrimTransformation : Transformation { var isRowBlank = true val prevColor = input[0, y] for (x in 1 until input.width) { - if (input[x, y] != prevColor) { + if (!isColorTheSame(input[x, y], prevColor)) { isRowBlank = false break } @@ -71,7 +75,7 @@ class TrimTransformation : Transformation { var isRowBlank = true val prevColor = input[0, y] for (x in 1 until input.width) { - if (input[x, y] != prevColor) { + if (!isColorTheSame(input[x, y], prevColor)) { isRowBlank = false break } @@ -93,4 +97,11 @@ class TrimTransformation : Transformation { override fun equals(other: Any?) = other is TrimTransformation override fun hashCode() = javaClass.hashCode() + + private fun isColorTheSame(@ColorInt a: Int, @ColorInt b: Int): Boolean { + return abs(a.red - b.red) <= tolerance && + abs(a.green - b.green) <= tolerance && + abs(a.blue - b.blue) <= tolerance && + abs(a.alpha - b.alpha) <= tolerance + } } \ No newline at end of file