From 12b94fae91a175edaeb7d5baf24ac6c1e8ad57f9 Mon Sep 17 00:00:00 2001 From: renhaoting <370797079@qq.com> Date: Wed, 5 Nov 2025 11:38:43 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A3=B0=E4=BC=98=E5=88=97=E8=A1=A8=E6=8E=A5?= =?UTF-8?q?=E5=8F=A3=E5=88=9D=E6=AD=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- VisualNovel/.gitignore | 3 +- VisualNovel/app/build.gradle.kts | 7 +- VisualNovel/app/src/main/AndroidManifest.xml | 1 - .../visualnovel/api/service/ChatService.kt | 12 + .../visualnovel/entity/response/ChatSound.kt | 54 +---- .../repository/api/ChatRepository.kt | 4 + .../remax/visualnovel/ui/chat/ChatActivity.kt | 28 ++- .../visualnovel/ui/chat/ChatViewModel.kt | 14 +- .../chat/setting/customui/ChatSettingView.kt | 14 +- .../ExpandSoundSelectView.kt | 9 +- .../expandableSelector/ExpandSoundSubView.kt | 22 +- .../visualnovel/utils/LanguageCodeUtil.kt | 229 ++++++++++++++++++ .../remax/visualnovel/utils/LanguageUtil.kt | 203 ++++++++++++++++ .../main/res/xml/network_security_config.xml | 3 + 14 files changed, 528 insertions(+), 75 deletions(-) create mode 100644 VisualNovel/app/src/main/java/com/remax/visualnovel/utils/LanguageCodeUtil.kt create mode 100644 VisualNovel/app/src/main/java/com/remax/visualnovel/utils/LanguageUtil.kt diff --git a/VisualNovel/.gitignore b/VisualNovel/.gitignore index 3cb54ba..443d506 100644 --- a/VisualNovel/.gitignore +++ b/VisualNovel/.gitignore @@ -14,4 +14,5 @@ /captures .externalNativeBuild .cxx -local.properties \ No newline at end of file +local.properties +/.idea/dictionaries/project.xml diff --git a/VisualNovel/app/build.gradle.kts b/VisualNovel/app/build.gradle.kts index 6b3ad59..da49644 100644 --- a/VisualNovel/app/build.gradle.kts +++ b/VisualNovel/app/build.gradle.kts @@ -141,7 +141,9 @@ android { buildConfigString("RECHAEGE_SERVICES", "https://test.xxxxx.ai/policy/recharge") 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("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") } } } diff --git a/VisualNovel/app/src/main/AndroidManifest.xml b/VisualNovel/app/src/main/AndroidManifest.xml index fb73b13..06caa87 100644 --- a/VisualNovel/app/src/main/AndroidManifest.xml +++ b/VisualNovel/app/src/main/AndroidManifest.xml @@ -42,7 +42,6 @@ android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/AppTheme" - android:usesCleartextTraffic="true" android:networkSecurityConfig="@xml/network_security_config" > diff --git a/VisualNovel/app/src/main/java/com/remax/visualnovel/api/service/ChatService.kt b/VisualNovel/app/src/main/java/com/remax/visualnovel/api/service/ChatService.kt index 3743d08..e4bea4a 100644 --- a/VisualNovel/app/src/main/java/com/remax/visualnovel/api/service/ChatService.kt +++ b/VisualNovel/app/src/main/java/com/remax/visualnovel/api/service/ChatService.kt @@ -15,6 +15,7 @@ import com.remax.visualnovel.entity.response.Album import com.remax.visualnovel.entity.response.Character import com.remax.visualnovel.entity.response.ChatBackground 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.HeartbeatLevelOutput 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.base.Response import retrofit2.http.Body +import retrofit2.http.GET import retrofit2.http.POST +import retrofit2.http.Query interface ChatService { @@ -156,4 +159,13 @@ interface ChatService { @POST("/web/ai-user/buy-heartbeat-val") suspend fun buyHeartbeatVal(@Body request: HeartbeatBuy): Response + + + + //------------------------------------------------------ + @GET(BuildConfig.API_BASE + "/tts/config/select/list") + suspend fun loadSoundList(@Query("language") page: Int = 1): Response> + + + } \ No newline at end of file diff --git a/VisualNovel/app/src/main/java/com/remax/visualnovel/entity/response/ChatSound.kt b/VisualNovel/app/src/main/java/com/remax/visualnovel/entity/response/ChatSound.kt index 41af03a..2b35f09 100644 --- a/VisualNovel/app/src/main/java/com/remax/visualnovel/entity/response/ChatSound.kt +++ b/VisualNovel/app/src/main/java/com/remax/visualnovel/entity/response/ChatSound.kt @@ -6,50 +6,14 @@ import kotlinx.parcelize.Parcelize @Parcelize 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" - } } diff --git a/VisualNovel/app/src/main/java/com/remax/visualnovel/repository/api/ChatRepository.kt b/VisualNovel/app/src/main/java/com/remax/visualnovel/repository/api/ChatRepository.kt index 4c35748..2636002 100644 --- a/VisualNovel/app/src/main/java/com/remax/visualnovel/repository/api/ChatRepository.kt +++ b/VisualNovel/app/src/main/java/com/remax/visualnovel/repository/api/ChatRepository.kt @@ -130,4 +130,8 @@ class ChatRepository @Inject constructor(private val chatService: ChatService) : chatService.buyHeartbeatVal(request) } + suspend fun loadSoundList(lang: Int) = executeHttp { + chatService.loadSoundList(lang) + } + } \ No newline at end of file diff --git a/VisualNovel/app/src/main/java/com/remax/visualnovel/ui/chat/ChatActivity.kt b/VisualNovel/app/src/main/java/com/remax/visualnovel/ui/chat/ChatActivity.kt index e1981a3..3ef33b2 100644 --- a/VisualNovel/app/src/main/java/com/remax/visualnovel/ui/chat/ChatActivity.kt +++ b/VisualNovel/app/src/main/java/com/remax/visualnovel/ui/chat/ChatActivity.kt @@ -25,6 +25,7 @@ import com.remax.visualnovel.databinding.ActivityActorChatBinding import com.remax.visualnovel.entity.request.ChatSetting import com.remax.visualnovel.event.model.OnLoginEvent import com.remax.visualnovel.extension.countDownCoroutines +import com.remax.visualnovel.extension.launchAndCollect import com.remax.visualnovel.extension.launchAndLoadingCollect import com.remax.visualnovel.extension.launchWithRequest 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.customui.HoldToTalkDialog 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.StatusBarUtil3 import com.remax.visualnovel.utils.setOnKeyboardHeightChangeListener @@ -47,7 +49,7 @@ import kotlin.getValue @Route(path = Routers.CHAT) class ChatActivity : BaseBindingActivity() { private var mMode = MODE_TEXT - private val chatViewModel by viewModels() + private val mViewModel by viewModels() private val mRecordAssist = RecordAssist() private val mImeHelper = ImeHelper() @@ -69,6 +71,22 @@ class ChatActivity : BaseBindingActivity() { } 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() { private fun showSelectModelDialog() { - with(chatViewModel) { + with(mViewModel) { fun createModelDialog() { val modelDialog = ChatModelDialog(this@ChatActivity) @@ -224,7 +242,7 @@ class ChatActivity : BaseBindingActivity() { if (chatModels.isNullOrEmpty()) { launchAndLoadingCollect({ - chatViewModel.getChatModels() + mViewModel.getChatModels() }) { onSuccess = { createModelDialog() @@ -314,11 +332,11 @@ class ChatActivity : BaseBindingActivity() { Timber.i("startRecording onStop: ${recordHelper.getFilename()}") launchAndLoadingCollect({ - chatViewModel.voiceASR(recordHelper.getFilename()) + mViewModel.voiceASR(recordHelper.getFilename()) }) { onSuccess = { if (!it?.content.isNullOrBlank()) { - chatViewModel.sendMsg(it.content, errorCallback = sendMsgErrorCallback) + mViewModel.sendMsg(it.content, errorCallback = sendMsgErrorCallback) } } } diff --git a/VisualNovel/app/src/main/java/com/remax/visualnovel/ui/chat/ChatViewModel.kt b/VisualNovel/app/src/main/java/com/remax/visualnovel/ui/chat/ChatViewModel.kt index ef9eb50..045322c 100644 --- a/VisualNovel/app/src/main/java/com/remax/visualnovel/ui/chat/ChatViewModel.kt +++ b/VisualNovel/app/src/main/java/com/remax/visualnovel/ui/chat/ChatViewModel.kt @@ -1,8 +1,6 @@ package com.remax.visualnovel.ui.chat -/** - * Created by HJW on 2025/8/13 - */ + import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.viewModelScope @@ -354,4 +352,14 @@ class ChatViewModel @Inject constructor( } } + + + + + //------------------------ new ------------------------ + suspend fun loadSoundList(lang: Int) = chatRepository.loadSoundList(lang) + + + + } diff --git a/VisualNovel/app/src/main/java/com/remax/visualnovel/ui/chat/setting/customui/ChatSettingView.kt b/VisualNovel/app/src/main/java/com/remax/visualnovel/ui/chat/setting/customui/ChatSettingView.kt index 4453d83..e80d830 100644 --- a/VisualNovel/app/src/main/java/com/remax/visualnovel/ui/chat/setting/customui/ChatSettingView.kt +++ b/VisualNovel/app/src/main/java/com/remax/visualnovel/ui/chat/setting/customui/ChatSettingView.kt @@ -172,7 +172,7 @@ class ChatSettingView @JvmOverloads constructor( fun initSoundSelectorView() { - val items = listOf( + /*val items = listOf( ChatSound( id = 1L, name = "Sound-1", @@ -212,9 +212,9 @@ class ChatSettingView @JvmOverloads constructor( imgUrl = "" ) ) - +*/ with(mBinding.soundActorSelector) { - setItems(items) + //setItems(items) setEventListener( object : ExpandSoundSelectView.IEventListener { override fun onItemSelected( @@ -228,6 +228,10 @@ class ChatSettingView @JvmOverloads constructor( if (isExpanded) scroll2Position(this@with) } + + override fun onFiltersChanged(sexValue: Int) { + //TODO("Not yet implemented") + } }) } } @@ -357,5 +361,9 @@ class ChatSettingView @JvmOverloads constructor( } + fun setSoundItems(newItems: List) { + mBinding.soundActorSelector.setItems(newItems) + } + } diff --git a/VisualNovel/app/src/main/java/com/remax/visualnovel/ui/chat/setting/customui/expandableSelector/ExpandSoundSelectView.kt b/VisualNovel/app/src/main/java/com/remax/visualnovel/ui/chat/setting/customui/expandableSelector/ExpandSoundSelectView.kt index f9555d9..f9cc09d 100644 --- a/VisualNovel/app/src/main/java/com/remax/visualnovel/ui/chat/setting/customui/expandableSelector/ExpandSoundSelectView.kt +++ b/VisualNovel/app/src/main/java/com/remax/visualnovel/ui/chat/setting/customui/expandableSelector/ExpandSoundSelectView.kt @@ -25,10 +25,11 @@ class ExpandSoundSelectView @JvmOverloads constructor( private var isExpanded = false private var animationDuration = 300 - private var mEventListener: IEventListener? = null + private lateinit var mEventListener: IEventListener interface IEventListener { fun onItemSelected(position: Int, item: ChatSound) fun onExpanded(isExpanded: Boolean) + fun onFiltersChanged(sexValue: Int) } fun setEventListener(listener: IEventListener) { mEventListener = listener @@ -49,11 +50,11 @@ class ExpandSoundSelectView @JvmOverloads constructor( mExpandView.setEventListener(object: ExpandSoundSubView.IEventListener { override fun onSoundSelected(sound: ChatSound) { - setTitleText(sound.name) + setTitleText(sound.nameLanguage) } - override fun onFilterChanged(filterType: Int) { - // + override fun onFilterChanged(sexValue: Int) { + mEventListener.onFiltersChanged(sexValue) } }) } diff --git a/VisualNovel/app/src/main/java/com/remax/visualnovel/ui/chat/setting/customui/expandableSelector/ExpandSoundSubView.kt b/VisualNovel/app/src/main/java/com/remax/visualnovel/ui/chat/setting/customui/expandableSelector/ExpandSoundSubView.kt index 4a2d3ad..4c7b07a 100644 --- a/VisualNovel/app/src/main/java/com/remax/visualnovel/ui/chat/setting/customui/expandableSelector/ExpandSoundSubView.kt +++ b/VisualNovel/app/src/main/java/com/remax/visualnovel/ui/chat/setting/customui/expandableSelector/ExpandSoundSubView.kt @@ -35,7 +35,7 @@ class ExpandSoundSubView @JvmOverloads constructor( interface IEventListener { 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) { val sound = getModel() - if (!sound.select) { + if (!sound.isSelected) { itemsRv.bindingAdapter.models?.filterIsInstance()?.forEach { item -> - item.select = item == sound + item.isSelected = item == sound } itemsRv.bindingAdapter.notifyDataSetChanged() mEventListener.onSoundSelected(sound) @@ -104,17 +104,17 @@ class ExpandSoundSubView @JvmOverloads constructor( onBind { val item = getModel() with(getBinding()) { - if (!item.imgUrl.isNullOrEmpty()) { - userAvatar.load(item.imgUrl) + if (item.headPortrait.isNotEmpty()) { + userAvatar.load(item.headPortrait) } 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 - 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) - tvSoundDescrible.setTextColor(ResUtil.getColor(if (item.isMale) R.color.male_text_color else R.color.female_text_color)) - tvSoundDescrible.text = item.description - ivSelect.setImageResource(if (item.select) R.drawable.sound_item_selected else R.drawable.sound_item_unselected) + tvSoundName.text = item.nameLanguage + 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.gender == 1) R.color.male_text_color else R.color.female_text_color)) + tvSoundDescrible.text = item.desLanguage + ivSelect.setImageResource(if (item.isSelected) R.drawable.sound_item_selected else R.drawable.sound_item_unselected) } } } diff --git a/VisualNovel/app/src/main/java/com/remax/visualnovel/utils/LanguageCodeUtil.kt b/VisualNovel/app/src/main/java/com/remax/visualnovel/utils/LanguageCodeUtil.kt new file mode 100644 index 0000000..228a1cf --- /dev/null +++ b/VisualNovel/app/src/main/java/com/remax/visualnovel/utils/LanguageCodeUtil.kt @@ -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 = 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 { + return languageCodeMap.values.sorted() + } + + /** + * 获取所有支持的语言信息 + */ + fun getSupportedLanguages(displayLocale: Locale = Locale.getDefault()): List { + 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 + ) +} \ No newline at end of file diff --git a/VisualNovel/app/src/main/java/com/remax/visualnovel/utils/LanguageUtil.kt b/VisualNovel/app/src/main/java/com/remax/visualnovel/utils/LanguageUtil.kt new file mode 100644 index 0000000..d3bddb3 --- /dev/null +++ b/VisualNovel/app/src/main/java/com/remax/visualnovel/utils/LanguageUtil.kt @@ -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 { + 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 { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + val localeList = LocaleList.getDefault() + val locales = mutableListOf() + 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) + } + } +} \ No newline at end of file diff --git a/VisualNovel/app/src/main/res/xml/network_security_config.xml b/VisualNovel/app/src/main/res/xml/network_security_config.xml index 7ed0cd4..427101c 100644 --- a/VisualNovel/app/src/main/res/xml/network_security_config.xml +++ b/VisualNovel/app/src/main/res/xml/network_security_config.xml @@ -2,5 +2,8 @@ 54.223.196.180 + 192.168.110.113 + +