声优列表接口初步
This commit is contained in:
parent
14c9cfa4a7
commit
12b94fae91
|
|
@ -15,3 +15,4 @@
|
|||
.externalNativeBuild
|
||||
.cxx
|
||||
local.properties
|
||||
/.idea/dictionaries/project.xml
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -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<Any>
|
||||
|
||||
|
||||
|
||||
|
||||
//------------------------------------------------------
|
||||
@GET(BuildConfig.API_BASE + "/tts/config/select/list")
|
||||
suspend fun loadSoundList(@Query("language") page: Int = 1): Response<List<ChatSound>>
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -6,50 +6,14 @@ import kotlinx.parcelize.Parcelize
|
|||
|
||||
@Parcelize
|
||||
data class ChatSound(
|
||||
|
||||
/**
|
||||
* 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
|
||||
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 {
|
||||
companion object {
|
||||
const val MEMBER = "MEMBER"
|
||||
const val HEARTBEAT_LEVEL = "HEARTBEAT_LEVEL"
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -130,4 +130,8 @@ class ChatRepository @Inject constructor(private val chatService: ChatService) :
|
|||
chatService.buyHeartbeatVal(request)
|
||||
}
|
||||
|
||||
suspend fun loadSoundList(lang: Int) = executeHttp {
|
||||
chatService.loadSoundList(lang)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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<ActivityActorChatBinding>() {
|
||||
private var mMode = MODE_TEXT
|
||||
private val chatViewModel by viewModels<ChatViewModel>()
|
||||
private val mViewModel by viewModels<ChatViewModel>()
|
||||
private val mRecordAssist = RecordAssist()
|
||||
private val mImeHelper = ImeHelper()
|
||||
|
||||
|
|
@ -69,6 +71,22 @@ class ChatActivity : BaseBindingActivity<ActivityActorChatBinding>() {
|
|||
}
|
||||
|
||||
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() {
|
||||
with(chatViewModel) {
|
||||
with(mViewModel) {
|
||||
fun createModelDialog() {
|
||||
val modelDialog = ChatModelDialog(this@ChatActivity)
|
||||
|
||||
|
|
@ -224,7 +242,7 @@ class ChatActivity : BaseBindingActivity<ActivityActorChatBinding>() {
|
|||
|
||||
if (chatModels.isNullOrEmpty()) {
|
||||
launchAndLoadingCollect({
|
||||
chatViewModel.getChatModels()
|
||||
mViewModel.getChatModels()
|
||||
}) {
|
||||
onSuccess = {
|
||||
createModelDialog()
|
||||
|
|
@ -314,11 +332,11 @@ class ChatActivity : BaseBindingActivity<ActivityActorChatBinding>() {
|
|||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<ChatSound>) {
|
||||
mBinding.soundActorSelector.setItems(newItems)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<ChatSound>()
|
||||
if (!sound.select) {
|
||||
if (!sound.isSelected) {
|
||||
itemsRv.bindingAdapter.models?.filterIsInstance<ChatSound>()?.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<ChatSound>()
|
||||
with(getBinding<LayoutItemSettingSoundBinding>()) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -2,5 +2,8 @@
|
|||
<network-security-config>
|
||||
<domain-config cleartextTrafficPermitted="true">
|
||||
<domain includeSubdomains="true">54.223.196.180</domain>
|
||||
<domain includeSubdomains="true">192.168.110.113</domain>
|
||||
</domain-config>
|
||||
|
||||
|
||||
</network-security-config>
|
||||
|
|
|
|||
Loading…
Reference in New Issue