Emoji flags in details

master
Koitharu 1 year ago
parent 7efc47724e
commit 14973298a0
Signed by: Koitharu
GPG Key ID: 676DEE768C17A9D7

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

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

@ -59,6 +59,7 @@ import org.koitharu.kotatsu.core.ui.BaseListAdapter
import org.koitharu.kotatsu.core.ui.OnContextClickListenerCompat import org.koitharu.kotatsu.core.ui.OnContextClickListenerCompat
import org.koitharu.kotatsu.core.ui.image.CoverSizeResolver import org.koitharu.kotatsu.core.ui.image.CoverSizeResolver
import org.koitharu.kotatsu.core.ui.image.FaviconDrawable 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.image.TextViewTarget
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.ui.sheet.BottomSheetCollapseCallback 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.util.ReversibleActionObserver
import org.koitharu.kotatsu.core.ui.widgets.ChipsView import org.koitharu.kotatsu.core.ui.widgets.ChipsView
import org.koitharu.kotatsu.core.util.FileSize 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.crossfade
import org.koitharu.kotatsu.core.util.ext.defaultPlaceholders import org.koitharu.kotatsu.core.util.ext.defaultPlaceholders
import org.koitharu.kotatsu.core.util.ext.drawable 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.enqueueWith
import org.koitharu.kotatsu.core.util.ext.ifNullOrEmpty import org.koitharu.kotatsu.core.util.ext.ifNullOrEmpty
import org.koitharu.kotatsu.core.util.ext.isTextTruncated import org.koitharu.kotatsu.core.util.ext.isTextTruncated
@ -173,7 +176,13 @@ class DetailsActivity :
viewModel.isStatsAvailable.observe(this, menuInvalidator) viewModel.isStatsAvailable.observe(this, menuInvalidator)
viewModel.remoteManga.observe(this, menuInvalidator) viewModel.remoteManga.observe(this, menuInvalidator)
viewModel.branches.observe(this) { 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 infoBinding.textViewTranslationLabel.isVisible = infoBinding.textViewTranslation.isVisible
} }
viewModel.chapters.observe(this, PrefetchObserver(this)) viewModel.chapters.observe(this, PrefetchObserver(this))
@ -193,8 +202,6 @@ class DetailsActivity :
override fun onClick(v: View) { override fun onClick(v: View) {
when (v.id) { when (v.id) {
// R.id.chip_branch -> showBranchPopupMenu(v)
R.id.textView_author -> { R.id.textView_author -> {
val manga = viewModel.manga.value ?: return val manga = viewModel.manga.value ?: return
router.openSearch(manga.source, manga.author ?: return) router.openSearch(manga.source, manga.author ?: return)
@ -462,14 +469,19 @@ class DetailsActivity :
} }
private fun onHistoryChanged(info: HistoryInfo, isLoading: Boolean) = with(infoBinding) { private fun onHistoryChanged(info: HistoryInfo, isLoading: Boolean) = with(infoBinding) {
textViewChapters.textAndVisible = if (isLoading) { textViewChapters.text = when {
null isLoading -> getString(R.string.loading_)
} else when { info.currentChapter >= 0 -> getString(
info.currentChapter >= 0 -> getString(R.string.chapter_d_of_d, info.currentChapter + 1, info.totalChapters) R.string.chapter_d_of_d,
info.currentChapter + 1,
info.totalChapters,
).withEstimatedTime(info.estimatedTime)
info.totalChapters == 0 -> getString(R.string.no_chapters) info.totalChapters == 0 -> getString(R.string.no_chapters)
info.totalChapters == -1 -> getString(R.string.error_occurred) info.totalChapters == -1 -> getString(R.string.error_occurred)
else -> resources.getQuantityString(R.plurals.chapters, info.totalChapters, info.totalChapters) else -> resources.getQuantityString(R.plurals.chapters, info.totalChapters, info.totalChapters)
}.withEstimatedTime(info.estimatedTime) .withEstimatedTime(info.estimatedTime)
}
textViewProgress.textAndVisible = if (info.percent <= 0f) { textViewProgress.textAndVisible = if (info.percent <= 0f) {
null null
} else { } else {
@ -482,8 +494,6 @@ class DetailsActivity :
textViewProgressLabel.isVisible = info.history != null textViewProgressLabel.isVisible = info.history != null
textViewProgress.isVisible = info.history != null textViewProgress.isVisible = info.history != null
progress.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) { private fun openReader(isIncognitoMode: Boolean) {

@ -2,6 +2,7 @@ package org.koitharu.kotatsu.details.ui.model
import org.koitharu.kotatsu.list.ui.ListModelDiffCallback import org.koitharu.kotatsu.list.ui.ListModelDiffCallback
import org.koitharu.kotatsu.list.ui.model.ListModel import org.koitharu.kotatsu.list.ui.model.ListModel
import java.util.Locale
data class MangaBranch( data class MangaBranch(
val name: String?, val name: String?,
@ -10,6 +11,8 @@ data class MangaBranch(
val isCurrent: Boolean, val isCurrent: Boolean,
) : ListModel { ) : ListModel {
val locale: Locale? by lazy(::findAppropriateLocale)
override fun areItemsTheSame(other: ListModel): Boolean { override fun areItemsTheSame(other: ListModel): Boolean {
return other is MangaBranch && other.name == name return other is MangaBranch && other.name == name
} }
@ -25,4 +28,16 @@ data class MangaBranch(
override fun toString(): String { override fun toString(): String {
return "$name: $count" 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)
}
}
} }

@ -93,6 +93,7 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="@dimen/margin_normal" android:layout_marginStart="@dimen/margin_normal"
android:layout_marginEnd="@dimen/margin_normal" android:layout_marginEnd="@dimen/margin_normal"
android:drawablePadding="4dp"
android:singleLine="true" android:singleLine="true"
android:textAppearance="?textAppearanceBodyMedium" android:textAppearance="?textAppearanceBodyMedium"
app:layout_constraintBaseline_toBaselineOf="@id/textView_translation_label" app:layout_constraintBaseline_toBaselineOf="@id/textView_translation_label"

Loading…
Cancel
Save