Emoji flags in details

This commit is contained in:
Koitharu
2025-01-04 13:53:13 +02:00
parent 7efc47724e
commit 14973298a0
5 changed files with 173 additions and 10 deletions

View File

@@ -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)
}
}
}
}

View File

@@ -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)
}
}
}

View File

@@ -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) {

View File

@@ -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)
}
}
}

View File

@@ -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"