From 14973298a027fef5ab048bf6eb03b5df68c045d2 Mon Sep 17 00:00:00 2001 From: Koitharu Date: Sat, 4 Jan 2025 13:53:13 +0200 Subject: [PATCH] Emoji flags in details --- .../kotatsu/core/ui/image/TextDrawable.kt | 102 ++++++++++++++++++ .../koitharu/kotatsu/core/util/LocaleUtils.kt | 35 ++++++ .../kotatsu/details/ui/DetailsActivity.kt | 30 ++++-- .../kotatsu/details/ui/model/MangaBranch.kt | 15 +++ .../main/res/layout/layout_details_table.xml | 1 + 5 files changed, 173 insertions(+), 10 deletions(-) create mode 100644 app/src/main/kotlin/org/koitharu/kotatsu/core/ui/image/TextDrawable.kt create mode 100644 app/src/main/kotlin/org/koitharu/kotatsu/core/util/LocaleUtils.kt diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/image/TextDrawable.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/image/TextDrawable.kt new file mode 100644 index 000000000..14cd84006 --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/image/TextDrawable.kt @@ -0,0 +1,102 @@ +package org.koitharu.kotatsu.core.ui.image + +import android.content.res.ColorStateList +import android.graphics.Canvas +import android.graphics.Color +import android.graphics.ColorFilter +import android.graphics.Paint +import android.graphics.PixelFormat +import android.graphics.PointF +import android.graphics.Rect +import android.graphics.drawable.Drawable +import android.widget.TextView +import androidx.core.graphics.PaintCompat + +class TextDrawable( + val text: String, +) : Drawable() { + + private val paint = Paint(Paint.ANTI_ALIAS_FLAG or Paint.SUBPIXEL_TEXT_FLAG) + private val textBounds = Rect() + private val textPoint = PointF() + + var textSize: Float + get() = paint.textSize + set(value) { + paint.textSize = value + measureTextBounds() + } + + var textColor: ColorStateList = ColorStateList.valueOf(Color.BLACK) + set(value) { + field = value + onStateChange(state) + } + + init { + measureTextBounds() + } + + override fun draw(canvas: Canvas) { + canvas.drawText(text, textPoint.x, textPoint.y, paint) + } + + override fun setAlpha(alpha: Int) { + paint.alpha = alpha + } + + override fun setColorFilter(colorFilter: ColorFilter?) { + paint.setColorFilter(colorFilter) + } + + override fun getOpacity(): Int = when (paint.alpha) { + 0 -> PixelFormat.TRANSPARENT + 255 -> PixelFormat.OPAQUE + else -> PixelFormat.TRANSLUCENT + } + + override fun onBoundsChange(bounds: Rect) { + textPoint.set( + bounds.exactCenterX() - textBounds.exactCenterX(), + bounds.exactCenterY() - textBounds.exactCenterY(), + ) + } + + override fun getIntrinsicWidth(): Int = textBounds.width() + + override fun getIntrinsicHeight(): Int = textBounds.height() + + override fun setDither(dither: Boolean) { + paint.isDither = dither + } + + override fun isStateful(): Boolean = textColor.isStateful + + override fun hasFocusStateSpecified(): Boolean = textColor.getColorForState( + intArrayOf(android.R.attr.state_focused), + textColor.defaultColor, + ) != textColor.defaultColor + + override fun onStateChange(state: IntArray): Boolean { + val prevColor = paint.color + paint.color = textColor.getColorForState(state, textColor.defaultColor) + return paint.color != prevColor + } + + private fun measureTextBounds() { + paint.getTextBounds(text, 0, text.length, textBounds) + onBoundsChange(bounds) + } + + companion object { + + fun compound(textView: TextView, text: String): TextDrawable? { + val drawable = TextDrawable(text) + drawable.textSize = textView.textSize + drawable.textColor = textView.textColors + return drawable.takeIf { + PaintCompat.hasGlyph(drawable.paint, text) + } + } + } +} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/util/LocaleUtils.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/LocaleUtils.kt new file mode 100644 index 000000000..514d016d2 --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/LocaleUtils.kt @@ -0,0 +1,35 @@ +package org.koitharu.kotatsu.core.util + +import android.graphics.Paint +import androidx.core.graphics.PaintCompat +import org.koitharu.kotatsu.core.util.ext.ifNullOrEmpty +import java.util.Locale + +object LocaleUtils { + + private val paint = Paint() + + fun getEmojiFlag(locale: Locale): String? { + val code = when (val c = locale.country.ifNullOrEmpty { locale.toLanguageTag() }.uppercase(Locale.ENGLISH)) { + "EN" -> "GB" + "JA" -> "JP" + else -> c + } + val emoji = countryCodeToEmojiFlag(code) + return if (PaintCompat.hasGlyph(paint, emoji)) { + emoji + } else { + null + } + } + + private fun countryCodeToEmojiFlag(countryCode: String): String { + return countryCode.map { char -> + Character.codePointAt("$char", 0) - 0x41 + 0x1F1E6 + }.map { codePoint -> + Character.toChars(codePoint) + }.joinToString(separator = "") { charArray -> + String(charArray) + } + } +} 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 3c764ecf1..09cf1fef3 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 @@ -59,6 +59,7 @@ import org.koitharu.kotatsu.core.ui.BaseListAdapter import org.koitharu.kotatsu.core.ui.OnContextClickListenerCompat import org.koitharu.kotatsu.core.ui.image.CoverSizeResolver import org.koitharu.kotatsu.core.ui.image.FaviconDrawable +import org.koitharu.kotatsu.core.ui.image.TextDrawable import org.koitharu.kotatsu.core.ui.image.TextViewTarget import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener import org.koitharu.kotatsu.core.ui.sheet.BottomSheetCollapseCallback @@ -66,9 +67,11 @@ import org.koitharu.kotatsu.core.ui.util.MenuInvalidator import org.koitharu.kotatsu.core.ui.util.ReversibleActionObserver import org.koitharu.kotatsu.core.ui.widgets.ChipsView import org.koitharu.kotatsu.core.util.FileSize +import org.koitharu.kotatsu.core.util.LocaleUtils import org.koitharu.kotatsu.core.util.ext.crossfade import org.koitharu.kotatsu.core.util.ext.defaultPlaceholders import org.koitharu.kotatsu.core.util.ext.drawable +import org.koitharu.kotatsu.core.util.ext.drawableStart import org.koitharu.kotatsu.core.util.ext.enqueueWith import org.koitharu.kotatsu.core.util.ext.ifNullOrEmpty import org.koitharu.kotatsu.core.util.ext.isTextTruncated @@ -173,7 +176,13 @@ class DetailsActivity : viewModel.isStatsAvailable.observe(this, menuInvalidator) viewModel.remoteManga.observe(this, menuInvalidator) viewModel.branches.observe(this) { - infoBinding.textViewTranslation.textAndVisible = it.singleOrNull()?.name + val branch = it.singleOrNull() + infoBinding.textViewTranslation.textAndVisible = branch?.name + infoBinding.textViewTranslation.drawableStart = branch?.locale?.let { + LocaleUtils.getEmojiFlag(it) + }?.let { + TextDrawable.compound(infoBinding.textViewTranslation, it) + } infoBinding.textViewTranslationLabel.isVisible = infoBinding.textViewTranslation.isVisible } viewModel.chapters.observe(this, PrefetchObserver(this)) @@ -193,8 +202,6 @@ class DetailsActivity : override fun onClick(v: View) { when (v.id) { - // R.id.chip_branch -> showBranchPopupMenu(v) - R.id.textView_author -> { val manga = viewModel.manga.value ?: return router.openSearch(manga.source, manga.author ?: return) @@ -462,14 +469,19 @@ class DetailsActivity : } private fun onHistoryChanged(info: HistoryInfo, isLoading: Boolean) = with(infoBinding) { - textViewChapters.textAndVisible = if (isLoading) { - null - } else when { - info.currentChapter >= 0 -> getString(R.string.chapter_d_of_d, info.currentChapter + 1, info.totalChapters) + textViewChapters.text = when { + isLoading -> getString(R.string.loading_) + info.currentChapter >= 0 -> getString( + R.string.chapter_d_of_d, + info.currentChapter + 1, + info.totalChapters, + ).withEstimatedTime(info.estimatedTime) + info.totalChapters == 0 -> getString(R.string.no_chapters) info.totalChapters == -1 -> getString(R.string.error_occurred) else -> resources.getQuantityString(R.plurals.chapters, info.totalChapters, info.totalChapters) - }.withEstimatedTime(info.estimatedTime) + .withEstimatedTime(info.estimatedTime) + } textViewProgress.textAndVisible = if (info.percent <= 0f) { null } else { @@ -482,8 +494,6 @@ class DetailsActivity : textViewProgressLabel.isVisible = info.history != null textViewProgress.isVisible = info.history != null progress.isVisible = info.history != null - // buttonRead.setProgress(info.percent.coerceIn(0f, 1f), !isFirstCall) - // buttonDownload?.isEnabled = info.isValid && info.canDownload } private fun openReader(isIncognitoMode: Boolean) { diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/model/MangaBranch.kt b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/model/MangaBranch.kt index 520f1de41..03eb72c95 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/model/MangaBranch.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/model/MangaBranch.kt @@ -2,6 +2,7 @@ package org.koitharu.kotatsu.details.ui.model import org.koitharu.kotatsu.list.ui.ListModelDiffCallback import org.koitharu.kotatsu.list.ui.model.ListModel +import java.util.Locale data class MangaBranch( val name: String?, @@ -10,6 +11,8 @@ data class MangaBranch( val isCurrent: Boolean, ) : ListModel { + val locale: Locale? by lazy(::findAppropriateLocale) + override fun areItemsTheSame(other: ListModel): Boolean { return other is MangaBranch && other.name == name } @@ -25,4 +28,16 @@ data class MangaBranch( override fun toString(): String { return "$name: $count" } + + private fun findAppropriateLocale(): Locale? { + if (name.isNullOrEmpty()) { + return null + } + return Locale.getAvailableLocales().find { lc -> + name.contains(lc.getDisplayName(lc), ignoreCase = true) || + name.contains(lc.getDisplayName(Locale.ENGLISH), ignoreCase = true) || + name.contains(lc.getDisplayLanguage(lc), ignoreCase = true) || + name.contains(lc.getDisplayLanguage(Locale.ENGLISH), ignoreCase = true) + } + } } diff --git a/app/src/main/res/layout/layout_details_table.xml b/app/src/main/res/layout/layout_details_table.xml index bffb61eab..0f4f905ae 100644 --- a/app/src/main/res/layout/layout_details_table.xml +++ b/app/src/main/res/layout/layout_details_table.xml @@ -93,6 +93,7 @@ android:layout_height="wrap_content" android:layout_marginStart="@dimen/margin_normal" android:layout_marginEnd="@dimen/margin_normal" + android:drawablePadding="4dp" android:singleLine="true" android:textAppearance="?textAppearanceBodyMedium" app:layout_constraintBaseline_toBaselineOf="@id/textView_translation_label"