多语言支持完整更新

pull/14/head
minxiwan 2 weeks ago
parent 3974c061b8
commit c5b9d779ad

@ -109,7 +109,7 @@ fun OnlineMsgApp(viewModel: ChatViewModel = viewModel()) {
var tab by rememberSaveable { mutableStateOf(MainTab.CHAT) } var tab by rememberSaveable { mutableStateOf(MainTab.CHAT) }
// 定义翻译函数 t // 定义翻译函数 t
fun t(key: String) = LanguageManager.getString(key, state.language) fun language(key: String) = LanguageManager.getString(key, state.language)
// 监听 ViewModel 发送的 UI 事件(如 Snackbar 消息) // 监听 ViewModel 发送的 UI 事件(如 Snackbar 消息)
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
@ -146,7 +146,7 @@ fun OnlineMsgApp(viewModel: ChatViewModel = viewModel()) {
NavigationBarItem( NavigationBarItem(
selected = tab == MainTab.CHAT, selected = tab == MainTab.CHAT,
onClick = { tab = MainTab.CHAT }, onClick = { tab = MainTab.CHAT },
label = { Text(t(MainTab.CHAT.labelKey), style = MaterialTheme.typography.labelSmall) }, label = { Text(language(MainTab.CHAT.labelKey), style = MaterialTheme.typography.labelSmall) },
icon = { icon = {
Icon( Icon(
imageVector = Icons.Rounded.Forum, imageVector = Icons.Rounded.Forum,
@ -158,7 +158,7 @@ fun OnlineMsgApp(viewModel: ChatViewModel = viewModel()) {
NavigationBarItem( NavigationBarItem(
selected = tab == MainTab.SETTINGS, selected = tab == MainTab.SETTINGS,
onClick = { tab = MainTab.SETTINGS }, onClick = { tab = MainTab.SETTINGS },
label = { Text(t(MainTab.SETTINGS.labelKey), style = MaterialTheme.typography.labelSmall) }, label = { Text(language(MainTab.SETTINGS.labelKey), style = MaterialTheme.typography.labelSmall) },
icon = { icon = {
Icon( Icon(
imageVector = Icons.Rounded.Settings, imageVector = Icons.Rounded.Settings,
@ -287,8 +287,8 @@ private fun ChatTab(
) { ) {
val listState = rememberLazyListState() val listState = rememberLazyListState()
// 定义翻译函数 t // 定义语言函数 language
fun t(key: String) = LanguageManager.getString(key, state.language) fun language(key: String) = LanguageManager.getString(key, state.language)
// 当消息列表新增消息时,自动滚动到底部 // 当消息列表新增消息时,自动滚动到底部
LaunchedEffect(state.visibleMessages.size) { LaunchedEffect(state.visibleMessages.size) {
@ -312,12 +312,12 @@ private fun ChatTab(
FilterChip( FilterChip(
selected = !state.directMode, selected = !state.directMode,
onClick = { onToggleDirectMode(false) }, onClick = { onToggleDirectMode(false) },
label = { Text(t("chat.broadcast")) } label = { Text(language("chat.broadcast")) }
) )
FilterChip( FilterChip(
selected = state.directMode, selected = state.directMode,
onClick = { onToggleDirectMode(true) }, onClick = { onToggleDirectMode(true) },
label = { Text(t("chat.private")) } label = { Text(language("chat.private")) }
) )
// 在这一行腾出的空间可以放置其他快捷操作,或者保持简洁 // 在这一行腾出的空间可以放置其他快捷操作,或者保持简洁
@ -336,8 +336,8 @@ private fun ChatTab(
value = state.targetKey, value = state.targetKey,
onValueChange = onTargetKeyChange, onValueChange = onTargetKeyChange,
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
label = { Text(t("chat.target_key")) }, label = { Text(language("chat.target_key")) },
placeholder = { Text(t("chat.target_key")) }, placeholder = { Text(language("chat.target_key")) },
maxLines = 3 maxLines = 3
) )
} }
@ -362,7 +362,7 @@ private fun ChatTab(
) )
) { ) {
Text( Text(
text = t("chat.empty_hint"), text = language("chat.empty_hint"),
modifier = Modifier.padding(12.dp), modifier = Modifier.padding(12.dp),
style = MaterialTheme.typography.bodyMedium style = MaterialTheme.typography.bodyMedium
) )
@ -390,7 +390,7 @@ private fun ChatTab(
value = state.draft, value = state.draft,
onValueChange = onDraftChange, onValueChange = onDraftChange,
modifier = Modifier.weight(1f), modifier = Modifier.weight(1f),
label = { Text(t("chat.input_placeholder")) }, label = { Text(language("chat.input_placeholder")) },
maxLines = 4, maxLines = 4,
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Send), keyboardOptions = KeyboardOptions(imeAction = ImeAction.Send),
keyboardActions = KeyboardActions( keyboardActions = KeyboardActions(
@ -405,7 +405,7 @@ private fun ChatTab(
) { ) {
Icon(Icons.AutoMirrored.Rounded.Send, contentDescription = null) Icon(Icons.AutoMirrored.Rounded.Send, contentDescription = null)
Spacer(Modifier.width(6.dp)) Spacer(Modifier.width(6.dp))
Text(if (state.sending) "..." else t("chat.send")) Text(if (state.sending) "..." else language("chat.send"))
} }
} }
} }
@ -593,7 +593,7 @@ private fun SettingsTab(
onUseDynamicColorChange: (Boolean) -> Unit, onUseDynamicColorChange: (Boolean) -> Unit,
onLanguageChange: (String) -> Unit onLanguageChange: (String) -> Unit
) { ) {
fun t(key: String) = LanguageManager.getString(key, state.language) fun language(key: String) = LanguageManager.getString(key, state.language)
val settingsCardModifier = Modifier.fillMaxWidth() val settingsCardModifier = Modifier.fillMaxWidth()
val settingsCardContentModifier = Modifier val settingsCardContentModifier = Modifier
@ -614,12 +614,12 @@ private fun SettingsTab(
modifier = settingsCardContentModifier, modifier = settingsCardContentModifier,
verticalArrangement = settingsCardContentSpacing verticalArrangement = settingsCardContentSpacing
) { ) {
Text(t("settings.personal"), style = MaterialTheme.typography.titleMedium) Text(language("settings.personal"), style = MaterialTheme.typography.titleMedium)
OutlinedTextField( OutlinedTextField(
value = state.displayName, value = state.displayName,
onValueChange = onDisplayNameChange, onValueChange = onDisplayNameChange,
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
label = { Text(t("settings.display_name")) }, label = { Text(language("settings.display_name")) },
maxLines = 1 maxLines = 1
) )
} }
@ -632,9 +632,9 @@ private fun SettingsTab(
modifier = settingsCardContentModifier, modifier = settingsCardContentModifier,
verticalArrangement = settingsCardContentSpacing verticalArrangement = settingsCardContentSpacing
) { ) {
Text("聊天数据", style = MaterialTheme.typography.titleMedium) Text(language("settings.chat_data"), style = MaterialTheme.typography.titleMedium)
OutlinedButton(onClick = onClearMessages) { OutlinedButton(onClick = onClearMessages) {
Text(t("settings.clear_msg")) Text(language("settings.clear_msg"))
} }
} }
} }
@ -646,21 +646,21 @@ private fun SettingsTab(
modifier = settingsCardContentModifier, modifier = settingsCardContentModifier,
verticalArrangement = settingsCardContentSpacing verticalArrangement = settingsCardContentSpacing
) { ) {
Text(t("settings.server"), style = MaterialTheme.typography.titleMedium) Text(language("settings.server"), style = MaterialTheme.typography.titleMedium)
OutlinedTextField( OutlinedTextField(
value = state.serverUrl, value = state.serverUrl,
onValueChange = onServerUrlChange, onValueChange = onServerUrlChange,
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
label = { Text(t("settings.server_url")) }, label = { Text(language("settings.server_url")) },
maxLines = 1 maxLines = 1
) )
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
Button(onClick = onSaveServer) { Text(t("settings.save_server")) } Button(onClick = onSaveServer) { Text(language("settings.save_server")) }
OutlinedButton(onClick = onRemoveServer) { Text(t("settings.remove_current")) } OutlinedButton(onClick = onRemoveServer) { Text(language("settings.remove_current")) }
} }
if (state.serverUrls.isNotEmpty()) { if (state.serverUrls.isNotEmpty()) {
HorizontalDivider() HorizontalDivider()
Text(t("settings.saved_servers"), style = MaterialTheme.typography.labelLarge) Text(language("settings.saved_servers"), style = MaterialTheme.typography.labelLarge)
LazyRow(horizontalArrangement = Arrangement.spacedBy(8.dp)) { LazyRow(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
items(state.serverUrls) { url -> items(state.serverUrls) { url ->
AssistChip( AssistChip(
@ -680,16 +680,16 @@ private fun SettingsTab(
modifier = settingsCardContentModifier, modifier = settingsCardContentModifier,
verticalArrangement = settingsCardContentSpacing verticalArrangement = settingsCardContentSpacing
) { ) {
Text(t("settings.identity"), style = MaterialTheme.typography.titleMedium) Text(language("settings.identity"), style = MaterialTheme.typography.titleMedium)
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
Button(onClick = onRevealPublicKey, enabled = !state.loadingPublicKey) { Button(onClick = onRevealPublicKey, enabled = !state.loadingPublicKey) {
Text(if (state.loadingPublicKey) "..." else t("settings.reveal_key")) Text(if (state.loadingPublicKey) "..." else language("settings.reveal_key"))
} }
OutlinedButton( OutlinedButton(
onClick = onCopyPublicKey, onClick = onCopyPublicKey,
enabled = state.myPublicKey.isNotBlank() enabled = state.myPublicKey.isNotBlank()
) { ) {
Text(t("settings.copy_key")) Text(language("settings.copy_key"))
} }
} }
OutlinedTextField( OutlinedTextField(
@ -697,7 +697,7 @@ private fun SettingsTab(
onValueChange = {}, onValueChange = {},
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
readOnly = true, readOnly = true,
label = { Text(t("settings.my_key")) }, label = { Text(language("settings.my_key")) },
maxLines = 4 maxLines = 4
) )
} }
@ -710,7 +710,7 @@ private fun SettingsTab(
modifier = settingsCardContentModifier, modifier = settingsCardContentModifier,
verticalArrangement = settingsCardContentSpacing verticalArrangement = settingsCardContentSpacing
) { ) {
Text(t("settings.language"), style = MaterialTheme.typography.titleMedium) Text(language("settings.language"), style = MaterialTheme.typography.titleMedium)
LazyRow(horizontalArrangement = Arrangement.spacedBy(8.dp)) { LazyRow(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
items(LanguageManager.supportedLanguages) { lang -> items(LanguageManager.supportedLanguages) { lang ->
FilterChip( FilterChip(
@ -737,7 +737,7 @@ private fun SettingsTab(
modifier = settingsCardContentModifier, modifier = settingsCardContentModifier,
verticalArrangement = settingsCardContentSpacing verticalArrangement = settingsCardContentSpacing
) { ) {
Text(t("settings.theme"), style = MaterialTheme.typography.titleMedium) Text(language("settings.theme"), style = MaterialTheme.typography.titleMedium)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
Row( Row(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
@ -747,18 +747,18 @@ private fun SettingsTab(
checked = state.useDynamicColor, checked = state.useDynamicColor,
onCheckedChange = onUseDynamicColorChange onCheckedChange = onUseDynamicColorChange
) )
Text(t("settings.dynamic_color")) Text(language("settings.dynamic_color"))
} }
} }
if (!state.useDynamicColor || Build.VERSION.SDK_INT < Build.VERSION_CODES.S) { if (!state.useDynamicColor || Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
Text(t("settings.preset_themes"), style = MaterialTheme.typography.labelLarge) Text(language("settings.preset_themes"), style = MaterialTheme.typography.labelLarge)
LazyRow(horizontalArrangement = Arrangement.spacedBy(8.dp)) { LazyRow(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
items(themeOptions) { option -> items(themeOptions) { option ->
val themeName = when (option.id) { val themeName = when (option.id) {
"blue" -> t("theme.blue") "blue" -> language("theme.blue")
"gray" -> t("theme.gray") "gray" -> language("theme.gray")
"green" -> t("theme.green") "green" -> language("theme.green")
"red" -> t("theme.red") "red" -> language("theme.red")
else -> option.name else -> option.name
} }
FilterChip( FilterChip(
@ -786,16 +786,16 @@ private fun SettingsTab(
modifier = settingsCardContentModifier, modifier = settingsCardContentModifier,
verticalArrangement = settingsCardContentSpacing verticalArrangement = settingsCardContentSpacing
) { ) {
Text(t("settings.diagnostics"), style = MaterialTheme.typography.titleMedium) Text(language("settings.diagnostics"), style = MaterialTheme.typography.titleMedium)
Text("${t("settings.status_hint")}${state.statusHint}") Text("${language("settings.status_hint")}${state.statusHint}")
Text("${t("settings.current_status")}${state.statusText}") Text("${language("settings.current_status")}${state.statusText}")
Text("${t("settings.cert_fingerprint")}${state.certFingerprint.ifBlank { "N/A" }}") Text("${language("settings.cert_fingerprint")}${state.certFingerprint.ifBlank { "N/A" }}")
Row( Row(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp) horizontalArrangement = Arrangement.spacedBy(8.dp)
) { ) {
Switch(checked = state.showSystemMessages, onCheckedChange = onToggleShowSystem) Switch(checked = state.showSystemMessages, onCheckedChange = onToggleShowSystem)
Text(t("settings.show_system")) Text(language("settings.show_system"))
} }
} }
} }

@ -48,6 +48,7 @@ import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.decodeFromJsonElement import kotlinx.serialization.json.decodeFromJsonElement
import kotlinx.serialization.json.encodeToJsonElement import kotlinx.serialization.json.encodeToJsonElement
import com.onlinemsg.client.util.LanguageManager
/** /**
* 单例管理类负责整个聊天会话的生命周期网络连接消息收发状态维护和持久化 * 单例管理类负责整个聊天会话的生命周期网络连接消息收发状态维护和持久化
@ -98,10 +99,11 @@ object ChatSessionManager {
private val socketListener = object : OnlineMsgSocketClient.Listener { private val socketListener = object : OnlineMsgSocketClient.Listener {
override fun onOpen() { override fun onOpen() {
scope.launch { scope.launch {
val lang = _uiState.value.language
_uiState.update { _uiState.update {
it.copy( it.copy(
status = ConnectionStatus.HANDSHAKING, status = ConnectionStatus.HANDSHAKING,
statusHint = "已连接,正在准备聊天..." statusHint = LanguageManager.getString("status_hint.handshaking", lang)
) )
} }
addSystemMessage("连接已建立") addSystemMessage("连接已建立")
@ -122,7 +124,8 @@ object ChatSessionManager {
override fun onBinaryMessage(payload: ByteArray) { override fun onBinaryMessage(payload: ByteArray) {
scope.launch { scope.launch {
if (_uiState.value.status == ConnectionStatus.HANDSHAKING) { if (_uiState.value.status == ConnectionStatus.HANDSHAKING) {
_uiState.update { it.copy(statusHint = "收到二进制握手帧,正在尝试解析...") } val lang = _uiState.value.language
_uiState.update { it.copy(statusHint = LanguageManager.getString("status_hint.received_binary_handshake", lang)) }
} }
val utf8 = runCatching { String(payload, StandardCharsets.UTF_8) }.getOrNull().orEmpty() val utf8 = runCatching { String(payload, StandardCharsets.UTF_8) }.getOrNull().orEmpty()
@ -152,10 +155,11 @@ object ChatSessionManager {
if (manualClose) return@launch if (manualClose) return@launch
val message = throwable.message?.takeIf { it.isNotBlank() } ?: "unknown" val message = throwable.message?.takeIf { it.isNotBlank() } ?: "unknown"
addSystemMessage("连接异常:$message") addSystemMessage("连接异常:$message")
val lang = _uiState.value.language
_uiState.update { _uiState.update {
it.copy( it.copy(
status = ConnectionStatus.ERROR, status = ConnectionStatus.ERROR,
statusHint = "连接异常,正在重试" statusHint = LanguageManager.getString("status_hint.connection_error_retrying", lang)
) )
} }
scheduleReconnect("连接异常") scheduleReconnect("连接异常")
@ -193,7 +197,8 @@ object ChatSessionManager {
themeId = pref.themeId, themeId = pref.themeId,
useDynamicColor = pref.useDynamicColor, useDynamicColor = pref.useDynamicColor,
language = pref.language, language = pref.language,
messages = historyMessages messages = historyMessages,
statusHint = LanguageManager.getString("status_hint.click_to_connect", pref.language)
) )
} }
// 如果上次会话启用了自动重连,则自动恢复连接 // 如果上次会话启用了自动重连,则自动恢复连接
@ -322,11 +327,12 @@ object ChatSessionManager {
} }
val nextUrls = ServerUrlFormatter.append(_uiState.value.serverUrls, normalized) val nextUrls = ServerUrlFormatter.append(_uiState.value.serverUrls, normalized)
val lang = _uiState.value.language
_uiState.update { _uiState.update {
it.copy( it.copy(
serverUrl = normalized, serverUrl = normalized,
serverUrls = nextUrls, serverUrls = nextUrls,
statusHint = "服务器地址已保存" statusHint = LanguageManager.getString("status_hint.server_saved", lang)
) )
} }
@ -351,11 +357,16 @@ object ChatSessionManager {
filtered filtered
} }
val lang = _uiState.value.language
_uiState.update { _uiState.update {
it.copy( it.copy(
serverUrls = nextUrls, serverUrls = nextUrls,
serverUrl = nextUrls.first(), serverUrl = nextUrls.first(),
statusHint = if (filtered.isEmpty()) "已恢复默认服务器地址" else "已移除当前服务器地址" statusHint = if (filtered.isEmpty()) {
LanguageManager.getString("status_hint.server_removed_default", lang)
} else {
LanguageManager.getString("status_hint.server_removed", lang)
}
) )
} }
@ -405,10 +416,11 @@ object ChatSessionManager {
val normalized = ServerUrlFormatter.normalize(state.serverUrl) val normalized = ServerUrlFormatter.normalize(state.serverUrl)
if (normalized.isBlank()) { if (normalized.isBlank()) {
val lang = _uiState.value.language
_uiState.update { _uiState.update {
it.copy( it.copy(
status = ConnectionStatus.ERROR, status = ConnectionStatus.ERROR,
statusHint = "请填写有效服务器地址" statusHint = LanguageManager.getString("status_hint.invalid_server_url", lang)
) )
} }
return return
@ -424,10 +436,11 @@ object ChatSessionManager {
cancelHelloTimeout() cancelHelloTimeout()
cancelAuthTimeout() cancelAuthTimeout()
val lang = _uiState.value.language
_uiState.update { _uiState.update {
it.copy( it.copy(
status = ConnectionStatus.CONNECTING, status = ConnectionStatus.CONNECTING,
statusHint = "正在连接服务器...", statusHint = LanguageManager.getString("status_hint.connecting", lang),
serverUrl = normalized, serverUrl = normalized,
certFingerprint = "" certFingerprint = ""
) )
@ -456,10 +469,11 @@ object ChatSessionManager {
cancelHelloTimeout() cancelHelloTimeout()
cancelAuthTimeout() cancelAuthTimeout()
socketClient.close(1000, "manual_close") socketClient.close(1000, "manual_close")
val lang = _uiState.value.language
_uiState.update { _uiState.update {
it.copy( it.copy(
status = ConnectionStatus.IDLE, status = ConnectionStatus.IDLE,
statusHint = "连接已关闭" statusHint = LanguageManager.getString("status_hint.disconnected", lang)
) )
} }
autoReconnectTriggered = false autoReconnectTriggered = false
@ -487,7 +501,8 @@ object ChatSessionManager {
val key = if (_uiState.value.directMode) _uiState.value.targetKey.trim() else "" val key = if (_uiState.value.directMode) _uiState.value.targetKey.trim() else ""
if (_uiState.value.directMode && key.isBlank()) { if (_uiState.value.directMode && key.isBlank()) {
_uiState.update { it.copy(statusHint = "请先填写目标公钥,再发送私聊消息") } val lang = _uiState.value.language
_uiState.update { it.copy(statusHint = LanguageManager.getString("status_hint.target_key_required", lang)) }
return@launch return@launch
} }
@ -563,7 +578,8 @@ object ChatSessionManager {
*/ */
private suspend fun handleIncomingMessage(rawText: String) { private suspend fun handleIncomingMessage(rawText: String) {
if (_uiState.value.status == ConnectionStatus.HANDSHAKING) { if (_uiState.value.status == ConnectionStatus.HANDSHAKING) {
_uiState.update { it.copy(statusHint = "已收到握手数据,正在解析...") } val lang = _uiState.value.language
_uiState.update { it.copy(statusHint = LanguageManager.getString("status_hint.hello_received", lang)) }
} }
val normalizedText = extractJsonCandidate(rawText) val normalizedText = extractJsonCandidate(rawText)
@ -595,10 +611,11 @@ object ChatSessionManager {
runCatching { json.decodeFromJsonElement<HelloDataDto>(it) }.getOrNull() runCatching { json.decodeFromJsonElement<HelloDataDto>(it) }.getOrNull()
} }
if (hello == null || hello.publicKey.isBlank() || hello.authChallenge.isBlank()) { if (hello == null || hello.publicKey.isBlank() || hello.authChallenge.isBlank()) {
val lang = _uiState.value.language
_uiState.update { _uiState.update {
it.copy( it.copy(
status = ConnectionStatus.ERROR, status = ConnectionStatus.ERROR,
statusHint = "握手失败:服务端响应不完整" statusHint = LanguageManager.getString("status_hint.handshake_failed_incomplete", lang)
) )
} }
return return
@ -609,14 +626,13 @@ object ChatSessionManager {
// 握手阶段收到非预期消息则报错 // 握手阶段收到非预期消息则报错
if (_uiState.value.status == ConnectionStatus.HANDSHAKING && plain != null) { if (_uiState.value.status == ConnectionStatus.HANDSHAKING && plain != null) {
_uiState.update { it.copy(statusHint = "握手失败:收到非预期消息") } val lang = _uiState.value.language
_uiState.update { it.copy(statusHint = LanguageManager.getString("status_hint.handshake_failed_unexpected", lang)) }
addSystemMessage("握手阶段收到非预期消息类型:${plain.type}") addSystemMessage("握手阶段收到非预期消息类型:${plain.type}")
} else if (_uiState.value.status == ConnectionStatus.HANDSHAKING && plain == null) { } else if (_uiState.value.status == ConnectionStatus.HANDSHAKING && plain == null) {
val preview = rawText val lang = _uiState.value.language
.replace("\n", " ") val preview = rawText.replace("\n", " ").replace("\r", " ").take(80)
.replace("\r", " ") _uiState.update { it.copy(statusHint = LanguageManager.getString("status_hint.handshake_failed_parse", lang)) }
.take(80)
_uiState.update { it.copy(statusHint = "握手失败:首包解析失败") }
addSystemMessage("握手包解析失败:$preview") addSystemMessage("握手包解析失败:$preview")
} }
@ -645,10 +661,11 @@ object ChatSessionManager {
private suspend fun handleServerHello(hello: HelloDataDto) { private suspend fun handleServerHello(hello: HelloDataDto) {
cancelHelloTimeout() cancelHelloTimeout()
serverPublicKey = hello.publicKey serverPublicKey = hello.publicKey
val lang = _uiState.value.language
_uiState.update { _uiState.update {
it.copy( it.copy(
status = ConnectionStatus.AUTHENTICATING, status = ConnectionStatus.AUTHENTICATING,
statusHint = "正在完成身份验证...", statusHint = LanguageManager.getString("status_hint.authenticating", lang),
certFingerprint = hello.certFingerprintSha256.orEmpty() certFingerprint = hello.certFingerprintSha256.orEmpty()
) )
} }
@ -657,10 +674,11 @@ object ChatSessionManager {
authTimeoutJob = scope.launch { authTimeoutJob = scope.launch {
delay(AUTH_TIMEOUT_MS) delay(AUTH_TIMEOUT_MS)
if (_uiState.value.status == ConnectionStatus.AUTHENTICATING) { if (_uiState.value.status == ConnectionStatus.AUTHENTICATING) {
val lang = _uiState.value.language
_uiState.update { _uiState.update {
it.copy( it.copy(
status = ConnectionStatus.ERROR, status = ConnectionStatus.ERROR,
statusHint = "连接超时,请重试" statusHint = LanguageManager.getString("status_hint.auth_timeout", lang)
) )
} }
addSystemMessage("认证超时,请检查网络后重试") addSystemMessage("认证超时,请检查网络后重试")
@ -674,10 +692,11 @@ object ChatSessionManager {
addSystemMessage("已发送认证请求") addSystemMessage("已发送认证请求")
}.onFailure { error -> }.onFailure { error ->
cancelAuthTimeout() cancelAuthTimeout()
val lang = _uiState.value.language
_uiState.update { _uiState.update {
it.copy( it.copy(
status = ConnectionStatus.ERROR, status = ConnectionStatus.ERROR,
statusHint = "认证失败" statusHint = LanguageManager.getString("status_hint.auth_failed", lang)
) )
} }
addSystemMessage("认证发送失败:${error.message ?: "unknown"}") addSystemMessage("认证发送失败:${error.message ?: "unknown"}")
@ -742,10 +761,11 @@ object ChatSessionManager {
cancelAuthTimeout() cancelAuthTimeout()
cancelReconnect() cancelReconnect()
reconnectAttempt = 0 reconnectAttempt = 0
val lang = _uiState.value.language
_uiState.update { _uiState.update {
it.copy( it.copy(
status = ConnectionStatus.READY, status = ConnectionStatus.READY,
statusHint = "已连接,可以开始聊天" statusHint = LanguageManager.getString("status_hint.ready", lang)
) )
} }
addSystemMessage("连接准备完成") addSystemMessage("连接准备完成")
@ -804,10 +824,11 @@ object ChatSessionManager {
if (fallbackUrl.isNotBlank()) { if (fallbackUrl.isNotBlank()) {
fallbackTried = true fallbackTried = true
connectedUrl = fallbackUrl connectedUrl = fallbackUrl
val lang = _uiState.value.language
_uiState.update { _uiState.update {
it.copy( it.copy(
status = ConnectionStatus.CONNECTING, status = ConnectionStatus.CONNECTING,
statusHint = "正在自动重试连接...", statusHint = LanguageManager.getString("status_hint.reconnecting", lang),
serverUrl = fallbackUrl serverUrl = fallbackUrl
) )
} }
@ -817,14 +838,16 @@ object ChatSessionManager {
} }
} }
val lang = _uiState.value.language
_uiState.update { _uiState.update {
it.copy( it.copy(
status = ConnectionStatus.ERROR, status = ConnectionStatus.ERROR,
statusHint = "连接已中断,正在重试" statusHint = LanguageManager.getString("status_hint.connection_interrupted_retrying", lang)
) )
} }
addSystemMessage("连接关闭 ($code)${reason.ifBlank { "连接中断" }}") addSystemMessage("连接关闭 ($code)${reason.ifBlank { "连接中断" }}")
scheduleReconnect("连接已中断") scheduleReconnect("连接已中断")
} }
/** /**
@ -935,10 +958,16 @@ object ChatSessionManager {
val exponential = 1 shl minOf(reconnectAttempt - 1, 5) val exponential = 1 shl minOf(reconnectAttempt - 1, 5)
val delaySeconds = minOf(MAX_RECONNECT_DELAY_SECONDS, exponential) val delaySeconds = minOf(MAX_RECONNECT_DELAY_SECONDS, exponential)
addSystemMessage("$reason${delaySeconds}s 后自动重连(第 $reconnectAttempt 次)") addSystemMessage("$reason${delaySeconds}s 后自动重连(第 $reconnectAttempt 次)")
val lang = _uiState.value.language
val hint = String.format(
LanguageManager.getString("status_hint.reconnect_countdown", lang),
delaySeconds,
reconnectAttempt
)
_uiState.update { _uiState.update {
it.copy( it.copy(
status = ConnectionStatus.ERROR, status = ConnectionStatus.ERROR,
statusHint = "${delaySeconds}s 后自动重连(第 $reconnectAttempt 次)" statusHint = hint
) )
} }
@ -950,10 +979,11 @@ object ChatSessionManager {
ServerUrlFormatter.normalize(_uiState.value.serverUrl) ServerUrlFormatter.normalize(_uiState.value.serverUrl)
} }
if (target.isBlank()) { if (target.isBlank()) {
val lang = _uiState.value.language
_uiState.update { _uiState.update {
it.copy( it.copy(
status = ConnectionStatus.ERROR, status = ConnectionStatus.ERROR,
statusHint = "重连失败:服务器地址无效" statusHint = LanguageManager.getString("status_hint.reconnect_failed_invalid_url", lang)
) )
} }
return@launch return@launch
@ -1015,10 +1045,11 @@ object ChatSessionManager {
delay(HELLO_TIMEOUT_MS) delay(HELLO_TIMEOUT_MS)
if (_uiState.value.status == ConnectionStatus.HANDSHAKING) { if (_uiState.value.status == ConnectionStatus.HANDSHAKING) {
val currentUrl = connectedUrl.ifBlank { "unknown" } val currentUrl = connectedUrl.ifBlank { "unknown" }
val lang = _uiState.value.language
_uiState.update { _uiState.update {
it.copy( it.copy(
status = ConnectionStatus.ERROR, status = ConnectionStatus.ERROR,
statusHint = "握手超时,请检查地址路径与反向代理" statusHint = LanguageManager.getString("status_hint.hello_timeout", lang)
) )
} }
addSystemMessage("握手超时:未收到服务端 publickey 首包(当前地址:$currentUrl") addSystemMessage("握手超时:未收到服务端 publickey 首包(当前地址:$currentUrl")

@ -1,6 +1,9 @@
package com.onlinemsg.client.ui package com.onlinemsg.client.ui
import androidx.compose.runtime.getValue
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import java.util.UUID import java.util.UUID
import com.onlinemsg.client.util.LanguageManager
/** /**
* 连接状态枚举 * 连接状态枚举
@ -109,17 +112,18 @@ data class ChatUiState(
val canSend: Boolean val canSend: Boolean
get() = status == ConnectionStatus.READY && draft.trim().isNotEmpty() && !sending get() = status == ConnectionStatus.READY && draft.trim().isNotEmpty() && !sending
/** /**
* 连接状态的简短文本描述 * 连接状态的简短文本描述
*/ */
val statusText: String val statusText: String
get() = when (status) { get() = when (status) {
ConnectionStatus.IDLE -> "未连接" ConnectionStatus.IDLE -> LanguageManager.getString("status.idle", language)
ConnectionStatus.CONNECTING, ConnectionStatus.CONNECTING,
ConnectionStatus.HANDSHAKING, ConnectionStatus.HANDSHAKING,
ConnectionStatus.AUTHENTICATING -> "连接中" ConnectionStatus.AUTHENTICATING -> LanguageManager.getString("status.connecting", language)
ConnectionStatus.READY -> "已连接" ConnectionStatus.READY -> LanguageManager.getString("status.ready", language)
ConnectionStatus.ERROR -> "异常断开" ConnectionStatus.ERROR -> LanguageManager.getString("status.error", language)
} }
/** /**

@ -12,6 +12,7 @@ object LanguageManager {
"tab.settings" to "设置", "tab.settings" to "设置",
"settings.personal" to "个人设置", "settings.personal" to "个人设置",
"settings.display_name" to "显示名称", "settings.display_name" to "显示名称",
"settings.chat_data" to "聊天数据",
"settings.server" to "服务器", "settings.server" to "服务器",
"settings.server_url" to "服务器地址", "settings.server_url" to "服务器地址",
"settings.save_server" to "保存地址", "settings.save_server" to "保存地址",
@ -29,8 +30,6 @@ object LanguageManager {
"settings.current_status" to "当前状态", "settings.current_status" to "当前状态",
"settings.cert_fingerprint" to "证书指纹", "settings.cert_fingerprint" to "证书指纹",
"settings.show_system" to "显示系统消息", "settings.show_system" to "显示系统消息",
"settings.connect" to "连接",
"settings.disconnect" to "断开",
"settings.clear_msg" to "清空消息", "settings.clear_msg" to "清空消息",
"settings.dynamic_color" to "使用动态颜色", "settings.dynamic_color" to "使用动态颜色",
"chat.broadcast" to "广播", "chat.broadcast" to "广播",
@ -46,13 +45,44 @@ object LanguageManager {
"theme.gray" to "商务灰", "theme.gray" to "商务灰",
"theme.green" to "翠绿", "theme.green" to "翠绿",
"theme.red" to "绯红", "theme.red" to "绯红",
"theme.warm" to "温暖" "theme.warm" to "温暖",
"top_bar.link" to "已连接",
"top_bar.dislink" to "未连接",
"top_bar.link_start" to "连接中",
"top_bar.error_dislink" to "异常断开",
"status_hint.ready" to "已连接,可以开始聊天",
"status_hint.click_to_connect" to "点击连接开始聊天",
"status_hint.handshaking" to "已连接,正在准备聊天...",
"status_hint.received_binary_handshake" to "收到二进制握手帧,正在尝试解析...",
"status_hint.connection_error_retrying" to "连接异常,正在重试",
"status_hint.invalid_server_url" to "请填写有效服务器地址",
"status_hint.connecting" to "正在连接服务器...",
"status_hint.disconnected" to "连接已关闭",
"status_hint.server_saved" to "服务器地址已保存",
"status_hint.server_removed_default" to "已恢复默认服务器地址",
"status_hint.server_removed" to "已移除当前服务器地址",
"status_hint.target_key_required" to "请先填写目标公钥,再发送私聊消息",
"status_hint.hello_received" to "已收到握手数据,正在解析...",
"status_hint.handshake_failed_incomplete" to "握手失败:服务端响应不完整",
"status_hint.handshake_failed_unexpected" to "握手失败:收到非预期消息",
"status_hint.handshake_failed_parse" to "握手失败:首包解析失败",
"status_hint.authenticating" to "正在完成身份验证...",
"status_hint.auth_timeout" to "连接超时,请重试",
"status_hint.auth_failed" to "认证失败",
"status_hint.ready" to "已连接,可以开始聊天",
"status_hint.reconnecting" to "正在自动重试连接...",
"status_hint.connection_interrupted_retrying" to "连接已中断,正在重试",
"status_hint.reconnect_countdown" to "%d秒后自动重连第 %d 次)",
"status_hint.reconnect_failed_invalid_url" to "重连失败:服务器地址无效",
"status_hint.hello_timeout" to "握手超时,请检查地址路径与反向代理"
), ),
"en" to mapOf( "en" to mapOf(
"tab.chat" to "Chat", "tab.chat" to "Chat",
"tab.settings" to "Settings", "tab.settings" to "Settings",
"settings.personal" to "Personal", "settings.personal" to "Personal",
"settings.display_name" to "Display Name", "settings.display_name" to "Display Name",
"settings.chat_data" to "Chat Data",
"settings.server" to "Server", "settings.server" to "Server",
"settings.server_url" to "Server Address", "settings.server_url" to "Server Address",
"settings.save_server" to "Save", "settings.save_server" to "Save",
@ -70,8 +100,6 @@ object LanguageManager {
"settings.current_status" to "Status", "settings.current_status" to "Status",
"settings.cert_fingerprint" to "Fingerprint", "settings.cert_fingerprint" to "Fingerprint",
"settings.show_system" to "Show System Messages", "settings.show_system" to "Show System Messages",
"settings.connect" to "Link",
"settings.disconnect" to "Dislink",
"settings.clear_msg" to "ClearMsg", "settings.clear_msg" to "ClearMsg",
"settings.dynamic_color" to "Use dynamic color", "settings.dynamic_color" to "Use dynamic color",
"chat.broadcast" to "Broadcast", "chat.broadcast" to "Broadcast",
@ -87,13 +115,44 @@ object LanguageManager {
"theme.gray" to "Business Gray", "theme.gray" to "Business Gray",
"theme.green" to "Green", "theme.green" to "Green",
"theme.red" to "Red", "theme.red" to "Red",
"theme.warm" to "Warm" "theme.warm" to "Warm",
"status.idle" to "Idle",
"status.connecting" to "Connecting",
"status.ready" to "Connected",
"status.error" to "Error",
"status_hint.ready" to "Ready, you can start chatting",
"status_hint.click_to_connect" to "Click connect to start chatting",
"status_hint.handshaking" to "Connected, preparing chat...",
"status_hint.received_binary_handshake" to "Received binary handshake frame, parsing...",
"status_hint.connection_error_retrying" to "Connection error, retrying",
"status_hint.invalid_server_url" to "Please enter a valid server address",
"status_hint.connecting" to "Connecting to server...",
"status_hint.disconnected" to "Connection closed",
"status_hint.server_saved" to "Server address saved",
"status_hint.server_removed_default" to "Restored default server address",
"status_hint.server_removed" to "Removed current server address",
"status_hint.target_key_required" to "Please enter target public key first",
"status_hint.hello_received" to "Handshake data received, parsing...",
"status_hint.handshake_failed_incomplete" to "Handshake failed: incomplete server response",
"status_hint.handshake_failed_unexpected" to "Handshake failed: unexpected message",
"status_hint.handshake_failed_parse" to "Handshake failed: first packet parse error",
"status_hint.authenticating" to "Authenticating...",
"status_hint.auth_timeout" to "Connection timeout, please retry",
"status_hint.auth_failed" to "Authentication failed",
"status_hint.ready" to "Connected, ready to chat",
"status_hint.reconnecting" to "Reconnecting...",
"status_hint.connection_interrupted_retrying" to "Connection interrupted, retrying",
"status_hint.reconnect_countdown" to "Reconnecting in %d seconds (attempt %d)",
"status_hint.reconnect_failed_invalid_url" to "Reconnect failed: invalid server address",
"status_hint.hello_timeout" to "Handshake timeout, check server path and reverse proxy"
), ),
"ja" to mapOf( "ja" to mapOf(
"tab.chat" to "チャット", "tab.chat" to "チャット",
"tab.settings" to "設定", "tab.settings" to "設定",
"settings.personal" to "個人設定", "settings.personal" to "個人設定",
"settings.display_name" to "表示名", "settings.display_name" to "表示名",
"settings.chat_data" to "チャットデータ",
"settings.server" to "サーバー", "settings.server" to "サーバー",
"settings.server_url" to "アドレス", "settings.server_url" to "アドレス",
"settings.save_server" to "保存", "settings.save_server" to "保存",
@ -111,8 +170,6 @@ object LanguageManager {
"settings.current_status" to "ステータス", "settings.current_status" to "ステータス",
"settings.cert_fingerprint" to "証明書指紋", "settings.cert_fingerprint" to "証明書指紋",
"settings.show_system" to "システムメッセージを表示", "settings.show_system" to "システムメッセージを表示",
"settings.connect" to "接続",
"settings.disconnect" to "切断",
"settings.clear_msg" to "履歴を消去", "settings.clear_msg" to "履歴を消去",
"settings.dynamic_color" to "動的カラーを使用", "settings.dynamic_color" to "動的カラーを使用",
"chat.broadcast" to "全体", "chat.broadcast" to "全体",
@ -128,13 +185,44 @@ object LanguageManager {
"theme.gray" to "ビジネスグレー", "theme.gray" to "ビジネスグレー",
"theme.green" to "グリーン", "theme.green" to "グリーン",
"theme.red" to "レッド", "theme.red" to "レッド",
"theme.warm" to "ウォーム" "theme.warm" to "ウォーム",
"status.idle" to "未接続",
"status.connecting" to "接続中",
"status.ready" to "接続済み",
"status.error" to "エラー",
"status_hint.ready" to "接続済み、チャットを開始できます",
"status_hint.click_to_connect" to "接続してチャットを開始",
"status_hint.handshaking" to "接続しました、準備中...",
"status_hint.received_binary_handshake" to "バイナリハンドシェイクを受信、解析中...",
"status_hint.connection_error_retrying" to "接続エラー、再試行中",
"status_hint.invalid_server_url" to "有効なサーバーアドレスを入力してください",
"status_hint.connecting" to "サーバーに接続中...",
"status_hint.disconnected" to "接続が切断されました",
"status_hint.server_saved" to "サーバーアドレスを保存しました",
"status_hint.server_removed_default" to "デフォルトサーバーに戻しました",
"status_hint.server_removed" to "現在のサーバーを削除しました",
"status_hint.target_key_required" to "相手の公開鍵を入力してください",
"status_hint.hello_received" to "ハンドシェイクデータを受信、解析中...",
"status_hint.handshake_failed_incomplete" to "ハンドシェイク失敗:サーバー応答が不完全",
"status_hint.handshake_failed_unexpected" to "ハンドシェイク失敗:予期しないメッセージ",
"status_hint.handshake_failed_parse" to "ハンドシェイク失敗:最初のパケット解析エラー",
"status_hint.authenticating" to "認証中...",
"status_hint.auth_timeout" to "接続タイムアウト、再試行してください",
"status_hint.auth_failed" to "認証に失敗しました",
"status_hint.ready" to "接続完了、チャットを開始できます",
"status_hint.reconnecting" to "自動再接続中...",
"status_hint.connection_interrupted_retrying" to "接続が切断されました、再試行中",
"status_hint.reconnect_countdown" to "%d秒後に再接続%d回目",
"status_hint.reconnect_failed_invalid_url" to "再接続失敗:サーバーアドレスが無効",
"status_hint.hello_timeout" to "ハンドシェイクタイムアウト、サーバーパスを確認してください"
), ),
"ko" to mapOf( "ko" to mapOf(
"tab.chat" to "채팅", "tab.chat" to "채팅",
"tab.settings" to "설정", "tab.settings" to "설정",
"settings.personal" to "개인 설정", "settings.personal" to "개인 설정",
"settings.display_name" to "표시 이름", "settings.display_name" to "표시 이름",
"settings.chat_data" to "채팅 데이터",
"settings.server" to "서버", "settings.server" to "서버",
"settings.server_url" to "서버 주소", "settings.server_url" to "서버 주소",
"settings.save_server" to "주소 저장", "settings.save_server" to "주소 저장",
@ -152,8 +240,6 @@ object LanguageManager {
"settings.current_status" to "현재 상태", "settings.current_status" to "현재 상태",
"settings.cert_fingerprint" to "인증서 지문", "settings.cert_fingerprint" to "인증서 지문",
"settings.show_system" to "시스템 메시지 표시", "settings.show_system" to "시스템 메시지 표시",
"settings.connect" to "연결",
"settings.disconnect" to "연결 끊기",
"settings.clear_msg" to "정보 삭제", "settings.clear_msg" to "정보 삭제",
"settings.dynamic_color" to "동적 색상 사용", "settings.dynamic_color" to "동적 색상 사용",
"chat.broadcast" to "브로드캐스트", "chat.broadcast" to "브로드캐스트",
@ -169,7 +255,37 @@ object LanguageManager {
"theme.gray" to "비즈니스 그레이", "theme.gray" to "비즈니스 그레이",
"theme.green" to "초록", "theme.green" to "초록",
"theme.red" to "빨강", "theme.red" to "빨강",
"theme.warm" to "따뜻함" "theme.warm" to "따뜻함",
"status.idle" to "연결 안 됨",
"status.connecting" to "연결 중",
"status.ready" to "연결됨",
"status.error" to "오류",
"status_hint.ready" to "연결됨, 채팅을 시작할 수 있습니다",
"status_hint.click_to_connect" to "연결하여 채팅 시작",
"status_hint.handshaking" to "연결됨, 채팅 준비 중...",
"status_hint.received_binary_handshake" to "바이너리 핸드셰이크 수신, 분석 중...",
"status_hint.connection_error_retrying" to "연결 오류, 재시도 중",
"status_hint.invalid_server_url" to "유효한 서버 주소를 입력하세요",
"status_hint.connecting" to "서버에 연결 중...",
"status_hint.disconnected" to "연결이 종료됨",
"status_hint.server_saved" to "서버 주소가 저장됨",
"status_hint.server_removed_default" to "기본 서버 주소로 복원됨",
"status_hint.server_removed" to "현재 서버 주소가 제거됨",
"status_hint.target_key_required" to "대상 공개키를 먼저 입력하세요",
"status_hint.hello_received" to "핸드셰이크 데이터 수신, 분석 중...",
"status_hint.handshake_failed_incomplete" to "핸드셰이크 실패: 서버 응답 불완전",
"status_hint.handshake_failed_unexpected" to "핸드셰이크 실패: 예상치 못한 메시지",
"status_hint.handshake_failed_parse" to "핸드셰이크 실패: 첫 패킷 구문 분석 오류",
"status_hint.authenticating" to "인증 중...",
"status_hint.auth_timeout" to "연결 시간 초과, 다시 시도하세요",
"status_hint.auth_failed" to "인증 실패",
"status_hint.ready" to "연결됨, 채팅 가능",
"status_hint.reconnecting" to "자동 재연결 중...",
"status_hint.connection_interrupted_retrying" to "연결이 끊어짐, 재시도 중",
"status_hint.reconnect_countdown" to "%d초 후 자동 재연결 (시도 %d회)",
"status_hint.reconnect_failed_invalid_url" to "재연결 실패: 서버 주소가 유효하지 않음",
"status_hint.hello_timeout" to "핸드셰이크 시간 초과, 서버 경로와 리버스 프록시를 확인하세요"
) )
) )

@ -1,5 +1,5 @@
plugins { plugins {
id("com.android.application") version "8.5.2" apply false id("com.android.application") version "8.13.2" apply false
id("org.jetbrains.kotlin.android") version "1.9.24" apply false id("org.jetbrains.kotlin.android") version "1.9.24" apply false
id("org.jetbrains.kotlin.plugin.serialization") version "1.9.24" apply false id("org.jetbrains.kotlin.plugin.serialization") version "1.9.24" apply false
id("com.google.devtools.ksp") version "1.9.24-1.0.20" apply false id("com.google.devtools.ksp") version "1.9.24-1.0.20" apply false

@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
networkTimeout=10000 networkTimeout=10000
validateDistributionUrl=true validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME

Loading…
Cancel
Save