声优列表接口初步

This commit is contained in:
renhaoting 2025-11-05 11:38:43 +08:00
parent 14c9cfa4a7
commit 12b94fae91
14 changed files with 528 additions and 75 deletions

View File

@ -14,4 +14,5 @@
/captures /captures
.externalNativeBuild .externalNativeBuild
.cxx .cxx
local.properties local.properties
/.idea/dictionaries/project.xml

View File

@ -141,7 +141,9 @@ android {
buildConfigString("RECHAEGE_SERVICES", "https://test.xxxxx.ai/policy/recharge") buildConfigString("RECHAEGE_SERVICES", "https://test.xxxxx.ai/policy/recharge")
buildConfigString("RTC_APP_ID", "689ade491323ae01797818e0-XXX-TODO") buildConfigString("RTC_APP_ID", "689ade491323ae01797818e0-XXX-TODO")
buildConfigString("API_BASE", "http://54.223.196.180:9090") //buildConfigString("API_BASE", "http://54.223.196.180:9090")
buildConfigString("API_BASE", "http://192.168.110.113:9090")
} }
@ -160,7 +162,8 @@ android {
buildConfigString("RECHAEGE_SERVICES", "https://test.xxxxx.ai/policy/recharge") buildConfigString("RECHAEGE_SERVICES", "https://test.xxxxx.ai/policy/recharge")
buildConfigString("RTC_APP_ID", "689ade491323ae01797818e0-XXX-TODO") buildConfigString("RTC_APP_ID", "689ade491323ae01797818e0-XXX-TODO")
buildConfigString("API_BASE", "http://54.223.196.180:9090") //buildConfigString("API_BASE", "http://54.223.196.180:9090")
buildConfigString("API_BASE", "http://192.168.110.113:9090")
} }
} }
} }

View File

@ -42,7 +42,6 @@
android:roundIcon="@mipmap/ic_launcher_round" android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true" android:supportsRtl="true"
android:theme="@style/AppTheme" android:theme="@style/AppTheme"
android:usesCleartextTraffic="true" android:usesCleartextTraffic="true"
android:networkSecurityConfig="@xml/network_security_config" android:networkSecurityConfig="@xml/network_security_config"
> >

View File

@ -15,6 +15,7 @@ import com.remax.visualnovel.entity.response.Album
import com.remax.visualnovel.entity.response.Character import com.remax.visualnovel.entity.response.Character
import com.remax.visualnovel.entity.response.ChatBackground import com.remax.visualnovel.entity.response.ChatBackground
import com.remax.visualnovel.entity.response.ChatSet import com.remax.visualnovel.entity.response.ChatSet
import com.remax.visualnovel.entity.response.ChatSound
import com.remax.visualnovel.entity.response.Friends import com.remax.visualnovel.entity.response.Friends
import com.remax.visualnovel.entity.response.HeartbeatLevelOutput import com.remax.visualnovel.entity.response.HeartbeatLevelOutput
import com.remax.visualnovel.entity.response.Pageable import com.remax.visualnovel.entity.response.Pageable
@ -22,7 +23,9 @@ import com.remax.visualnovel.entity.response.Token
import com.remax.visualnovel.entity.response.VoiceASR import com.remax.visualnovel.entity.response.VoiceASR
import com.remax.visualnovel.entity.response.base.Response import com.remax.visualnovel.entity.response.base.Response
import retrofit2.http.Body import retrofit2.http.Body
import retrofit2.http.GET
import retrofit2.http.POST import retrofit2.http.POST
import retrofit2.http.Query
interface ChatService { interface ChatService {
@ -156,4 +159,13 @@ interface ChatService {
@POST("/web/ai-user/buy-heartbeat-val") @POST("/web/ai-user/buy-heartbeat-val")
suspend fun buyHeartbeatVal(@Body request: HeartbeatBuy): Response<Any> suspend fun buyHeartbeatVal(@Body request: HeartbeatBuy): Response<Any>
//------------------------------------------------------
@GET(BuildConfig.API_BASE + "/tts/config/select/list")
suspend fun loadSoundList(@Query("language") page: Int = 1): Response<List<ChatSound>>
} }

View File

@ -6,50 +6,14 @@ import kotlinx.parcelize.Parcelize
@Parcelize @Parcelize
data class ChatSound( data class ChatSound(
val ttsId: Long,
val language: Int,
val desLanguage: String,
val gender: Int,
val rules: Int,
val nameLanguage: String,
val headPortrait: String,
var isSelected: Boolean = false
) : Parcelable {
/**
* id
*/
val id: Long,
/**
* 图片url
*/
val imgUrl: String?,
/**
* 名称
*/
val name: String,
/**
* 名称
*/
val description: String,
/**
* actor 性别
*/
val isMale: Boolean,
/**
* 当前用户是否解锁 false:未解锁true:解锁
*/
val isUnlock: Boolean? = null,
/**
* 解锁心动等级 类型为HEARTBEAT_LEVEL时才有用
*/
val unlockHeartbeatLevel: String? = null,
/**
* 解锁类型 MEMBER:会员 HEARTBEAT_LEVEL:心动等级
*/
val unlockType: String? = null,
var select: Boolean = false
) : Parcelable {
companion object {
const val MEMBER = "MEMBER"
const val HEARTBEAT_LEVEL = "HEARTBEAT_LEVEL"
}
} }

View File

@ -130,4 +130,8 @@ class ChatRepository @Inject constructor(private val chatService: ChatService) :
chatService.buyHeartbeatVal(request) chatService.buyHeartbeatVal(request)
} }
suspend fun loadSoundList(lang: Int) = executeHttp {
chatService.loadSoundList(lang)
}
} }

View File

@ -25,6 +25,7 @@ import com.remax.visualnovel.databinding.ActivityActorChatBinding
import com.remax.visualnovel.entity.request.ChatSetting import com.remax.visualnovel.entity.request.ChatSetting
import com.remax.visualnovel.event.model.OnLoginEvent import com.remax.visualnovel.event.model.OnLoginEvent
import com.remax.visualnovel.extension.countDownCoroutines import com.remax.visualnovel.extension.countDownCoroutines
import com.remax.visualnovel.extension.launchAndCollect
import com.remax.visualnovel.extension.launchAndLoadingCollect import com.remax.visualnovel.extension.launchAndLoadingCollect
import com.remax.visualnovel.extension.launchWithRequest import com.remax.visualnovel.extension.launchWithRequest
import com.remax.visualnovel.extension.setMargin import com.remax.visualnovel.extension.setMargin
@ -34,6 +35,7 @@ import com.remax.visualnovel.ui.chat.customui.ChatCallView
import com.remax.visualnovel.ui.chat.setting.model.ChatModelDialog import com.remax.visualnovel.ui.chat.setting.model.ChatModelDialog
import com.remax.visualnovel.ui.chat.customui.HoldToTalkDialog import com.remax.visualnovel.ui.chat.customui.HoldToTalkDialog
import com.remax.visualnovel.ui.chat.customui.InputPanel import com.remax.visualnovel.ui.chat.customui.InputPanel
import com.remax.visualnovel.utils.LanguageUtil
import com.remax.visualnovel.utils.RecordHelper import com.remax.visualnovel.utils.RecordHelper
import com.remax.visualnovel.utils.StatusBarUtil3 import com.remax.visualnovel.utils.StatusBarUtil3
import com.remax.visualnovel.utils.setOnKeyboardHeightChangeListener import com.remax.visualnovel.utils.setOnKeyboardHeightChangeListener
@ -47,7 +49,7 @@ import kotlin.getValue
@Route(path = Routers.CHAT) @Route(path = Routers.CHAT)
class ChatActivity : BaseBindingActivity<ActivityActorChatBinding>() { class ChatActivity : BaseBindingActivity<ActivityActorChatBinding>() {
private var mMode = MODE_TEXT private var mMode = MODE_TEXT
private val chatViewModel by viewModels<ChatViewModel>() private val mViewModel by viewModels<ChatViewModel>()
private val mRecordAssist = RecordAssist() private val mRecordAssist = RecordAssist()
private val mImeHelper = ImeHelper() private val mImeHelper = ImeHelper()
@ -69,6 +71,22 @@ class ChatActivity : BaseBindingActivity<ActivityActorChatBinding>() {
} }
override fun initData() { override fun initData() {
loadSoundDatas()
}
private fun loadSoundDatas() {
launchAndCollect({
mViewModel.loadSoundList(LanguageUtil.instance().getCurrentLanguageCode())
}) {
onSuccess = {
val dataList = it?: emptyList()
binding.settingView.setSoundItems(dataList)
}
onComplete = {
}
}
} }
@ -202,7 +220,7 @@ class ChatActivity : BaseBindingActivity<ActivityActorChatBinding>() {
private fun showSelectModelDialog() { private fun showSelectModelDialog() {
with(chatViewModel) { with(mViewModel) {
fun createModelDialog() { fun createModelDialog() {
val modelDialog = ChatModelDialog(this@ChatActivity) val modelDialog = ChatModelDialog(this@ChatActivity)
@ -224,7 +242,7 @@ class ChatActivity : BaseBindingActivity<ActivityActorChatBinding>() {
if (chatModels.isNullOrEmpty()) { if (chatModels.isNullOrEmpty()) {
launchAndLoadingCollect({ launchAndLoadingCollect({
chatViewModel.getChatModels() mViewModel.getChatModels()
}) { }) {
onSuccess = { onSuccess = {
createModelDialog() createModelDialog()
@ -314,11 +332,11 @@ class ChatActivity : BaseBindingActivity<ActivityActorChatBinding>() {
Timber.i("startRecording onStop: ${recordHelper.getFilename()}") Timber.i("startRecording onStop: ${recordHelper.getFilename()}")
launchAndLoadingCollect({ launchAndLoadingCollect({
chatViewModel.voiceASR(recordHelper.getFilename()) mViewModel.voiceASR(recordHelper.getFilename())
}) { }) {
onSuccess = { onSuccess = {
if (!it?.content.isNullOrBlank()) { if (!it?.content.isNullOrBlank()) {
chatViewModel.sendMsg(it.content, errorCallback = sendMsgErrorCallback) mViewModel.sendMsg(it.content, errorCallback = sendMsgErrorCallback)
} }
} }
} }

View File

@ -1,8 +1,6 @@
package com.remax.visualnovel.ui.chat package com.remax.visualnovel.ui.chat
/**
* Created by HJW on 2025/8/13
*/
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
@ -354,4 +352,14 @@ class ChatViewModel @Inject constructor(
} }
} }
//------------------------ new ------------------------
suspend fun loadSoundList(lang: Int) = chatRepository.loadSoundList(lang)
} }

View File

@ -172,7 +172,7 @@ class ChatSettingView @JvmOverloads constructor(
fun initSoundSelectorView() { fun initSoundSelectorView() {
val items = listOf( /*val items = listOf(
ChatSound( ChatSound(
id = 1L, id = 1L,
name = "Sound-1", name = "Sound-1",
@ -212,9 +212,9 @@ class ChatSettingView @JvmOverloads constructor(
imgUrl = "" imgUrl = ""
) )
) )
*/
with(mBinding.soundActorSelector) { with(mBinding.soundActorSelector) {
setItems(items) //setItems(items)
setEventListener( setEventListener(
object : ExpandSoundSelectView.IEventListener { object : ExpandSoundSelectView.IEventListener {
override fun onItemSelected( override fun onItemSelected(
@ -228,6 +228,10 @@ class ChatSettingView @JvmOverloads constructor(
if (isExpanded) if (isExpanded)
scroll2Position(this@with) scroll2Position(this@with)
} }
override fun onFiltersChanged(sexValue: Int) {
//TODO("Not yet implemented")
}
}) })
} }
} }
@ -357,5 +361,9 @@ class ChatSettingView @JvmOverloads constructor(
} }
fun setSoundItems(newItems: List<ChatSound>) {
mBinding.soundActorSelector.setItems(newItems)
}
} }

View File

@ -25,10 +25,11 @@ class ExpandSoundSelectView @JvmOverloads constructor(
private var isExpanded = false private var isExpanded = false
private var animationDuration = 300 private var animationDuration = 300
private var mEventListener: IEventListener? = null private lateinit var mEventListener: IEventListener
interface IEventListener { interface IEventListener {
fun onItemSelected(position: Int, item: ChatSound) fun onItemSelected(position: Int, item: ChatSound)
fun onExpanded(isExpanded: Boolean) fun onExpanded(isExpanded: Boolean)
fun onFiltersChanged(sexValue: Int)
} }
fun setEventListener(listener: IEventListener) { fun setEventListener(listener: IEventListener) {
mEventListener = listener mEventListener = listener
@ -49,11 +50,11 @@ class ExpandSoundSelectView @JvmOverloads constructor(
mExpandView.setEventListener(object: ExpandSoundSubView.IEventListener { mExpandView.setEventListener(object: ExpandSoundSubView.IEventListener {
override fun onSoundSelected(sound: ChatSound) { override fun onSoundSelected(sound: ChatSound) {
setTitleText(sound.name) setTitleText(sound.nameLanguage)
} }
override fun onFilterChanged(filterType: Int) { override fun onFilterChanged(sexValue: Int) {
// mEventListener.onFiltersChanged(sexValue)
} }
}) })
} }

View File

@ -35,7 +35,7 @@ class ExpandSoundSubView @JvmOverloads constructor(
interface IEventListener { interface IEventListener {
fun onSoundSelected(sound: ChatSound) fun onSoundSelected(sound: ChatSound)
fun onFilterChanged(filterType: Int) fun onFilterChanged(setValue: Int)
} }
@ -92,9 +92,9 @@ class ExpandSoundSubView @JvmOverloads constructor(
onClick(R.id.item_root) { onClick(R.id.item_root) {
val sound = getModel<ChatSound>() val sound = getModel<ChatSound>()
if (!sound.select) { if (!sound.isSelected) {
itemsRv.bindingAdapter.models?.filterIsInstance<ChatSound>()?.forEach { item -> itemsRv.bindingAdapter.models?.filterIsInstance<ChatSound>()?.forEach { item ->
item.select = item == sound item.isSelected = item == sound
} }
itemsRv.bindingAdapter.notifyDataSetChanged() itemsRv.bindingAdapter.notifyDataSetChanged()
mEventListener.onSoundSelected(sound) mEventListener.onSoundSelected(sound)
@ -104,17 +104,17 @@ class ExpandSoundSubView @JvmOverloads constructor(
onBind { onBind {
val item = getModel<ChatSound>() val item = getModel<ChatSound>()
with(getBinding<LayoutItemSettingSoundBinding>()) { with(getBinding<LayoutItemSettingSoundBinding>()) {
if (!item.imgUrl.isNullOrEmpty()) { if (item.headPortrait.isNotEmpty()) {
userAvatar.load(item.imgUrl) userAvatar.load(item.headPortrait)
} else { } else {
userAvatar.setImageResource(if (item.isMale) R.mipmap.ic_gender_male else R.mipmap.ic_gender_female) userAvatar.setImageResource(if (item.gender == 1) R.mipmap.ic_gender_male else R.mipmap.ic_gender_female)
} }
tvSoundName.text = item.name tvSoundName.text = item.nameLanguage
itemRoot.setBgColorDirectly(bgColor = ResUtil.getColor(if (item.isMale) R.color.male_bg else R.color.female_bg), radius = ResUtil.getPixelSize(R.dimen.dp_10).toFloat(), bgDrawable = null) itemRoot.setBgColorDirectly(bgColor = ResUtil.getColor(if (item.gender == 1) R.color.male_bg else R.color.female_bg), radius = ResUtil.getPixelSize(R.dimen.dp_10).toFloat(), bgDrawable = null)
tvSoundDescrible.setTextColor(ResUtil.getColor(if (item.isMale) R.color.male_text_color else R.color.female_text_color)) tvSoundDescrible.setTextColor(ResUtil.getColor(if (item.gender == 1) R.color.male_text_color else R.color.female_text_color))
tvSoundDescrible.text = item.description tvSoundDescrible.text = item.desLanguage
ivSelect.setImageResource(if (item.select) R.drawable.sound_item_selected else R.drawable.sound_item_unselected) ivSelect.setImageResource(if (item.isSelected) R.drawable.sound_item_selected else R.drawable.sound_item_unselected)
} }
} }
} }

View File

@ -0,0 +1,229 @@
package com.remax.visualnovel.utils
import java.util.*
class LanguageCodeUtil private constructor() {
companion object {
@Volatile
private var instance: LanguageCodeUtil? = null
fun getInstance(): LanguageCodeUtil {
return instance ?: synchronized(this) {
instance ?: LanguageCodeUtil().also { instance = it }
}
}
}
/**
* 语言编号映射表ISO 639-1 语言代码 -> 编号
* 编号规则按语言使用频率和地区重要性分配
*/
private val languageCodeMap: Map<String, Int> = mapOf(
// 主要语言1-99
"zh" to 1, // 中文
"en" to 2, // 英语
"es" to 3, // 西班牙语
"hi" to 4, // 印地语
"ar" to 5, // 阿拉伯语
"pt" to 6, // 葡萄牙语
"bn" to 7, // 孟加拉语
"ru" to 8, // 俄语
"ja" to 9, // 日语
"pa" to 10, // 旁遮普语
// 欧洲语言100-199
"fr" to 101, // 法语
"de" to 102, // 德语
"it" to 103, // 意大利语
"tr" to 104, // 土耳其语
"pl" to 105, // 波兰语
"uk" to 106, // 乌克兰语
"ro" to 107, // 罗马尼亚语
"nl" to 108, // 荷兰语
"hu" to 109, // 匈牙利语
"el" to 110, // 希腊语
"cs" to 111, // 捷克语
"sv" to 112, // 瑞典语
"da" to 113, // 丹麦语
"fi" to 114, // 芬兰语
"sk" to 115, // 斯洛伐克语
"bg" to 116, // 保加利亚语
"hr" to 117, // 克罗地亚语
"lt" to 118, // 立陶宛语
"sl" to 119, // 斯洛文尼亚语
"lv" to 120, // 拉脱维亚语
"et" to 121, // 爱沙尼亚语
// 亚洲语言200-299
"ko" to 201, // 韩语
"vi" to 202, // 越南语
"th" to 203, // 泰语
"fa" to 204, // 波斯语
"ur" to 205, // 乌尔都语
"ms" to 206, // 马来语
"id" to 207, // 印尼语
"tl" to 208, // 他加禄语
"my" to 209, // 缅甸语
"km" to 210, // 高棉语
"lo" to 211, // 老挝语
"ne" to 212, // 尼泊尔语
"si" to 213, // 僧伽罗语
"mn" to 214, // 蒙古语
"ka" to 215, // 格鲁吉亚语
"hy" to 216, // 亚美尼亚语
// 非洲语言300-399
"sw" to 301, // 斯瓦希里语
"am" to 302, // 阿姆哈拉语
"ha" to 303, // 豪萨语
"yo" to 304, // 约鲁巴语
"ig" to 305, // 伊博语
"zu" to 306, // 祖鲁语
"xh" to 307, // 科萨语
"rw" to 308, // 卢旺达语
// 其他语言400-499
"he" to 401, // 希伯来语
"yi" to 402, // 意第绪语
"cy" to 403, // 威尔士语
"ga" to 404, // 爱尔兰语
"is" to 405, // 冰岛语
"mt" to 406, // 马耳他语
// 中文变体500-599
"zh-CN" to 501, // 简体中文
"zh-TW" to 502, // 繁体中文(台湾)
"zh-HK" to 503, // 繁体中文(香港)
"zh-SG" to 504, // 简体中文(新加坡)
"zh-MO" to 505, // 繁体中文(澳门)
// 英语变体600-699
"en-US" to 601, // 美式英语
"en-GB" to 602, // 英式英语
"en-CA" to 603, // 加拿大英语
"en-AU" to 604, // 澳大利亚英语
"en-IN" to 605, // 印度英语
)
/**
* 获取语言的Int编号
* @param locale 语言Locale对象
* @return 语言编号如果未找到返回默认值0表示未知语言
*/
fun getLanguageCode(locale: Locale): Int {
// 优先尝试完整语言标签(包含地区)
val fullTag = locale.toLanguageTag()
if (languageCodeMap.containsKey(fullTag)) {
return languageCodeMap[fullTag]!!
}
// 尝试语言代码(不含地区)
val languageTag = locale.language
if (languageCodeMap.containsKey(languageTag)) {
return languageCodeMap[languageTag]!!
}
// 尝试构建语言-地区组合
val countrySpecificTag = "${locale.language}-${locale.country}"
if (languageCodeMap.containsKey(countrySpecificTag)) {
return languageCodeMap[countrySpecificTag]!!
}
// 未找到,返回未知语言代码
return 0
}
/**
* 根据编号获取语言Locale对象
* @param code 语言编号
* @return 对应的Locale对象如果未找到返回系统默认Locale
*/
fun getLocaleByCode(code: Int): Locale {
val languageEntry = languageCodeMap.entries.find { it.value == code }
return if (languageEntry != null) {
val languageTag = languageEntry.key
if (languageTag.contains("-")) {
// 包含地区的语言标签
val parts = languageTag.split("-")
if (parts.size >= 2) {
Locale(parts[0], parts[1])
} else {
Locale(languageTag)
}
} else {
// 只有语言代码
Locale(languageTag)
}
} else {
// 未找到,返回系统默认
Locale.getDefault()
}
}
/**
* 获取语言名称根据编号
*/
fun getLanguageNameByCode(code: Int, displayLocale: Locale = Locale.getDefault()): String {
val locale = getLocaleByCode(code)
return locale.getDisplayName(displayLocale)
}
/**
* 检查编号是否有效
*/
fun isValidLanguageCode(code: Int): Boolean {
return code != 0 && languageCodeMap.containsValue(code)
}
/**
* 获取所有支持的语言编号列表
*/
fun getSupportedLanguageCodes(): List<Int> {
return languageCodeMap.values.sorted()
}
/**
* 获取所有支持的语言信息
*/
fun getSupportedLanguages(displayLocale: Locale = Locale.getDefault()): List<LanguageInfo> {
return languageCodeMap.entries.map { (languageTag, code) ->
val locale = if (languageTag.contains("-")) {
val parts = languageTag.split("-")
Locale(parts[0], parts[1])
} else {
Locale(languageTag)
}
LanguageInfo(
code = code,
languageTag = languageTag,
locale = locale,
displayName = locale.getDisplayName(displayLocale),
nativeName = locale.getDisplayName(locale),
isRTL = isRtlLanguage(locale)
)
}.sortedBy { it.code }
}
/**
* 判断语言是否RTL
*/
private fun isRtlLanguage(locale: Locale): Boolean {
val rtlLanguages = arrayOf("ar", "he", "fa", "ur", "ps", "sd", "ug", "yi")
return rtlLanguages.contains(locale.language)
}
/**
* 语言信息数据类
*/
data class LanguageInfo(
val code: Int,
val languageTag: String,
val locale: Locale,
val displayName: String,
val nativeName: String,
val isRTL: Boolean
)
}

View File

@ -0,0 +1,203 @@
package com.remax.visualnovel.utils
import android.content.Context
import android.os.Build
import android.os.LocaleList
import java.util.*
class LanguageUtil private constructor() {
companion object {
@Volatile
private var instance: LanguageUtil? = null
fun instance(): LanguageUtil {
return instance ?: synchronized(this) {
instance ?: LanguageUtil().also { instance = it }
}
}
}
private val languageCodeUtil = LanguageCodeUtil.getInstance()
/**
* 获取系统Locale对象
*/
private fun getSystemLocale(): Locale {
return Locale.getDefault()
}
/**
* 获取应用当前显示的语言考虑应用内语言切换
*/
fun getAppCurrentLanguage(context: Context): Locale {
val config = context.resources.configuration
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
config.locales.get(0)
} else {
@Suppress("DEPRECATION")
config.locale
}
}
/**
* 获取当前系统语言的Int编号
*/
fun getCurrentLanguageCode(): Int {
return languageCodeUtil.getLanguageCode(getSystemLocale())
}
/**
* 获取指定Locale的Int编号
*/
fun getLanguageCode(locale: Locale): Int {
return languageCodeUtil.getLanguageCode(locale)
}
/**
* 根据编号获取语言信息
*/
fun getLanguageInfoByCode(code: Int): LanguageCodeUtil.LanguageInfo? {
return languageCodeUtil.getSupportedLanguages().find { it.code == code }
}
/**
* 获取系统支持的所有语言列表带编号
*/
fun getSystemSupportedLanguagesWithCodes(): List<LanguageCodeUtil.LanguageInfo> {
val systemLocales = getSystemSupportedLocales()
return systemLocales.map { locale ->
val code = languageCodeUtil.getLanguageCode(locale)
LanguageCodeUtil.LanguageInfo(
code = code,
languageTag = locale.toLanguageTag(),
locale = locale,
displayName = locale.getDisplayName(getSystemLocale()),
nativeName = locale.getDisplayName(locale),
isRTL = isRtlLanguage(locale)
)
}.sortedBy { it.code }
}
/**
* 检查系统是否支持特定语言编号
*/
fun isLanguageCodeSupported(code: Int): Boolean {
return languageCodeUtil.isValidLanguageCode(code) &&
getSystemSupportedLocales().any {
languageCodeUtil.getLanguageCode(it) == code
}
}
/**
* 获取语言信息详情增强版包含编号
*/
data class EnhancedLanguageInfo(
val languageCode: Int, // 语言编号
val languageTag: String, // 语言标签
val locale: Locale, // Locale对象
val displayName: String, // 本地化显示名称
val englishName: String, // 英文名称
val isRTL: Boolean, // 是否从右到左
val flagEmoji: String, // 国旗emoji
val direction: String // 语言方向
)
fun getEnhancedLanguageInfo(locale: Locale = getSystemLocale()): EnhancedLanguageInfo {
val code = languageCodeUtil.getLanguageCode(locale)
return EnhancedLanguageInfo(
languageCode = code,
languageTag = locale.toLanguageTag(),
locale = locale,
displayName = locale.getDisplayName(locale),
englishName = locale.getDisplayName(Locale.ENGLISH),
isRTL = isRtlLanguage(locale),
flagEmoji = getLanguageFlagEmoji(locale),
direction = if (isRtlLanguage(locale)) "RTL" else "LTR"
)
}
/**
* 获取系统支持的所有语言列表
*/
fun getSystemSupportedLocales(): List<Locale> {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
val localeList = LocaleList.getDefault()
val locales = mutableListOf<Locale>()
for (i in 0 until localeList.size()) {
locales.add(localeList[i])
}
locales
} else {
listOf(Locale.getDefault())
}
}
/**
* 检查系统是否支持特定语言
*/
fun isLanguageSupported(languageCode: String): Boolean {
return getSystemSupportedLocales().any { it.language == languageCode }
}
/**
* 判断语言是否从右到左
*/
private fun isRtlLanguage(locale: Locale): Boolean {
val rtlLanguages = arrayOf("ar", "he", "fa", "ur", "ps", "sd", "ug", "yi")
return rtlLanguages.contains(locale.language)
}
/**
* 获取语言对应的国旗emoji
*/
fun getLanguageFlagEmoji(locale: Locale): String {
return when (locale.country.uppercase()) {
"CN" -> "🇨🇳"
"US", "GB" -> "🇺🇸"
"JP" -> "🇯🇵"
"KR" -> "🇰🇷"
"FR" -> "🇫🇷"
"DE" -> "🇩🇪"
"ES" -> "🇪🇸"
"IT" -> "🇮🇹"
"RU" -> "🇷🇺"
"BR" -> "🇧🇷"
"IN" -> "🇮🇳"
"CA" -> "🇨🇦"
"AU" -> "🇦🇺"
"MX" -> "🇲🇽"
"TW" -> "🇹🇼"
"HK" -> "🇭🇰"
"MO" -> "🇲🇴"
"SG" -> "🇸🇬"
else -> "🌐"
}
}
/**
* 格式化语言显示
*/
fun formatLanguageDisplay(locale: Locale): String {
return when (locale.language) {
"zh" -> when (locale.country.uppercase()) {
"CN", "SG" -> "中文 (简体)"
"TW", "HK", "MO" -> "中文 (繁體)"
else -> "中文"
}
"en" -> "English"
"ja" -> "日本語"
"ko" -> "한국어"
"fr" -> "Français"
"de" -> "Deutsch"
"es" -> "Español"
"ru" -> "Русский"
"ar" -> "العربية"
"pt" -> "Português"
"hi" -> "हिन्दी"
"th" -> "ไทย"
"vi" -> "Tiếng Việt"
else -> locale.getDisplayName(locale)
}
}
}

View File

@ -2,5 +2,8 @@
<network-security-config> <network-security-config>
<domain-config cleartextTrafficPermitted="true"> <domain-config cleartextTrafficPermitted="true">
<domain includeSubdomains="true">54.223.196.180</domain> <domain includeSubdomains="true">54.223.196.180</domain>
<domain includeSubdomains="true">192.168.110.113</domain>
</domain-config> </domain-config>
</network-security-config> </network-security-config>