From e9d519554b165c10599d501b8a2983e9d028afd3 Mon Sep 17 00:00:00 2001 From: emilia-t Date: Sun, 8 Mar 2026 12:33:09 +0800 Subject: [PATCH] =?UTF-8?q?1=E4=BF=AE=E5=A4=8D=E6=B7=B1=E8=89=B2=E6=A8=A1?= =?UTF-8?q?=E5=BC=8F=E6=98=BE=E7=A4=BA=E9=97=AE=E9=A2=982=E8=B0=83?= =?UTF-8?q?=E6=95=B4=E9=83=A8=E5=88=86UI3=E5=A2=9E=E5=8A=A0=E4=B8=BB?= =?UTF-8?q?=E9=A2=98=E6=94=AF=E6=8C=814=E5=A2=9E=E5=8A=A0=E5=A4=9A?= =?UTF-8?q?=E8=AF=AD=E8=A8=80=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .idea/gradle.xml | 12 + .idea/studiobot.xml | 6 + android-client/app/build.gradle.kts | 2 +- .../preferences/UserPreferencesRepository.kt | 16 +- .../com/onlinemsg/client/ui/ChatScreen.kt | 617 ++++++++---------- .../onlinemsg/client/ui/ChatSessionManager.kt | 20 +- .../com/onlinemsg/client/ui/ChatUiState.kt | 6 +- .../com/onlinemsg/client/ui/ChatViewModel.kt | 3 +- .../com/onlinemsg/client/ui/theme/Color.kt | 18 +- .../com/onlinemsg/client/ui/theme/Theme.kt | 67 +- .../onlinemsg/client/util/LanguageManager.kt | 188 ++++++ 11 files changed, 573 insertions(+), 382 deletions(-) create mode 100644 .idea/gradle.xml create mode 100644 .idea/studiobot.xml create mode 100644 android-client/app/src/main/java/com/onlinemsg/client/util/LanguageManager.kt diff --git a/.idea/gradle.xml b/.idea/gradle.xml new file mode 100644 index 0000000..361eddd --- /dev/null +++ b/.idea/gradle.xml @@ -0,0 +1,12 @@ + + + + + + \ No newline at end of file diff --git a/.idea/studiobot.xml b/.idea/studiobot.xml new file mode 100644 index 0000000..539e3b8 --- /dev/null +++ b/.idea/studiobot.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/android-client/app/build.gradle.kts b/android-client/app/build.gradle.kts index 320a3f3..7581e5a 100644 --- a/android-client/app/build.gradle.kts +++ b/android-client/app/build.gradle.kts @@ -13,7 +13,7 @@ android { minSdk = 26 targetSdk = 34 versionCode = 1 - versionName = "1.0.0.1" + versionName = "1.0.0.2" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" vectorDrawables { diff --git a/android-client/app/src/main/java/com/onlinemsg/client/data/preferences/UserPreferencesRepository.kt b/android-client/app/src/main/java/com/onlinemsg/client/data/preferences/UserPreferencesRepository.kt index e039044..9e7974b 100644 --- a/android-client/app/src/main/java/com/onlinemsg/client/data/preferences/UserPreferencesRepository.kt +++ b/android-client/app/src/main/java/com/onlinemsg/client/data/preferences/UserPreferencesRepository.kt @@ -22,8 +22,9 @@ data class UserPreferences( val showSystemMessages: Boolean, val directMode: Boolean, val shouldAutoReconnect: Boolean, - val themeId: String = "default", - val useDynamicColor: Boolean = true + val themeId: String = "blue", + val useDynamicColor: Boolean = true, + val language: String = "zh" // 默认中文 ) class UserPreferencesRepository( @@ -47,8 +48,9 @@ class UserPreferencesRepository( showSystemMessages = prefs[KEY_SHOW_SYSTEM_MESSAGES] ?: false, directMode = prefs[KEY_DIRECT_MODE] ?: false, shouldAutoReconnect = prefs[KEY_SHOULD_AUTO_RECONNECT] ?: false, - themeId = prefs[KEY_THEME_ID] ?: "default", - useDynamicColor = prefs[KEY_USE_DYNAMIC_COLOR] ?: true + themeId = prefs[KEY_THEME_ID] ?: "blue", + useDynamicColor = prefs[KEY_USE_DYNAMIC_COLOR] ?: true, + language = prefs[KEY_LANGUAGE] ?: "zh" ) } @@ -58,6 +60,11 @@ class UserPreferencesRepository( } } + suspend fun setLanguage(language: String) { + context.dataStore.edit { prefs -> + prefs[KEY_LANGUAGE] = language + } + } suspend fun setUseDynamicColor(enabled: Boolean) { context.dataStore.edit { prefs -> @@ -147,5 +154,6 @@ class UserPreferencesRepository( val KEY_SHOULD_AUTO_RECONNECT: Preferences.Key = booleanPreferencesKey("should_auto_reconnect") val KEY_THEME_ID: Preferences.Key = stringPreferencesKey("theme_id") val KEY_USE_DYNAMIC_COLOR: Preferences.Key = booleanPreferencesKey("use_dynamic_color") + val KEY_LANGUAGE: Preferences.Key = stringPreferencesKey("language") } } diff --git a/android-client/app/src/main/java/com/onlinemsg/client/ui/ChatScreen.kt b/android-client/app/src/main/java/com/onlinemsg/client/ui/ChatScreen.kt index e641d90..f84fc1e 100644 --- a/android-client/app/src/main/java/com/onlinemsg/client/ui/ChatScreen.kt +++ b/android-client/app/src/main/java/com/onlinemsg/client/ui/ChatScreen.kt @@ -1,10 +1,11 @@ package com.onlinemsg.client.ui +import android.annotation.SuppressLint import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.consumeWindowInsets import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.navigationBarsPadding @@ -28,6 +29,9 @@ import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.rounded.Send import androidx.compose.material.icons.rounded.ContentCopy +import androidx.compose.material.icons.rounded.Forum +import androidx.compose.material.icons.rounded.Settings +import androidx.compose.material.icons.rounded.Language import androidx.compose.material3.AssistChip import androidx.compose.material3.Button import androidx.compose.material3.Card @@ -48,6 +52,7 @@ import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Switch import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar +import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue @@ -71,17 +76,16 @@ import com.onlinemsg.client.ui.theme.OnlineMsgTheme import java.time.Instant import java.time.ZoneId import java.time.format.DateTimeFormatter -import com.onlinemsg.client.ui.theme.themeOptions -import com.onlinemsg.client.ui.theme.ThemeOption +import com.onlinemsg.client.ui.theme.themeOptions +import com.onlinemsg.client.util.LanguageManager /** * 主界面底部导航栏的选项卡枚举。 - * @property label 选项卡显示的文本 */ -private enum class MainTab(val label: String) { - CHAT("聊天"), - SETTINGS("设置") +private enum class MainTab(val labelKey: String) { + CHAT("tab.chat"), + SETTINGS("tab.settings") } /** @@ -91,6 +95,7 @@ private enum class MainTab(val label: String) { */ @Composable fun OnlineMsgApp(viewModel: ChatViewModel = viewModel()) { + val state by viewModel.uiState.collectAsStateWithLifecycle() OnlineMsgTheme( darkTheme = isSystemInDarkTheme(), // 仍可跟随系统 @@ -98,11 +103,13 @@ fun OnlineMsgApp(viewModel: ChatViewModel = viewModel()) { useDynamicColor = state.useDynamicColor ) { - val state by viewModel.uiState.collectAsStateWithLifecycle() val clipboard = LocalClipboardManager.current val snackbarHostState = remember { SnackbarHostState() } var tab by rememberSaveable { mutableStateOf(MainTab.CHAT) } + // 定义翻译函数 t + fun t(key: String) = LanguageManager.getString(key, state.language) + // 监听 ViewModel 发送的 UI 事件(如 Snackbar 消息) LaunchedEffect(Unit) { viewModel.events.collect { event -> @@ -124,18 +131,36 @@ fun OnlineMsgApp(viewModel: ChatViewModel = viewModel()) { ) }, bottomBar = { - NavigationBar(modifier = Modifier.navigationBarsPadding()) { + NavigationBar( + modifier = Modifier + .navigationBarsPadding() + .height(64.dp), + containerColor = MaterialTheme.colorScheme.surface, + tonalElevation = 0.dp + ) { NavigationBarItem( selected = tab == MainTab.CHAT, onClick = { tab = MainTab.CHAT }, - label = { Text(MainTab.CHAT.label) }, - icon = {} + label = { Text(t(MainTab.CHAT.labelKey), style = MaterialTheme.typography.labelSmall) }, + icon = { + Icon( + imageVector = Icons.Rounded.Forum, + contentDescription = null, + modifier = Modifier.size(22.dp) + ) + } ) NavigationBarItem( selected = tab == MainTab.SETTINGS, onClick = { tab = MainTab.SETTINGS }, - label = { Text(MainTab.SETTINGS.label) }, - icon = {} + label = { Text(t(MainTab.SETTINGS.labelKey), style = MaterialTheme.typography.labelSmall) }, + icon = { + Icon( + imageVector = Icons.Rounded.Settings, + contentDescription = null, + modifier = Modifier.size(22.dp) + ) + } ) } }, @@ -185,7 +210,8 @@ fun OnlineMsgApp(viewModel: ChatViewModel = viewModel()) { onDisconnect = viewModel::disconnect, onClearMessages = viewModel::clearMessages, onThemeChange = viewModel::updateTheme, - onUseDynamicColorChange = viewModel::updateUseDynamicColor + onUseDynamicColorChange = viewModel::updateUseDynamicColor, + onLanguageChange = viewModel::updateLanguage ) } } @@ -195,7 +221,7 @@ fun OnlineMsgApp(viewModel: ChatViewModel = viewModel()) { /** * 应用程序顶部栏,显示标题和当前连接状态徽章。 - * @param statusText 状态文本(如“已连接”) + * @param statusText 状态文本 * @param statusColor 状态指示点的颜色 */ @OptIn(ExperimentalMaterial3Api::class) @@ -207,8 +233,8 @@ private fun AppTopBar( TopAppBar( title = { Text( - text = "OnlineMsg Chat", - style = MaterialTheme.typography.titleLarge + text = "OnlineMsg", + style = MaterialTheme.typography.titleMedium ) }, actions = { @@ -218,30 +244,25 @@ private fun AppTopBar( leadingIcon = { Box( modifier = Modifier - .width(10.dp) - .height(10.dp) + .size(8.dp) .background(statusColor, RoundedCornerShape(999.dp)) ) - } + }, + modifier = Modifier.height(32.dp) ) Spacer(modifier = Modifier.width(12.dp)) - } + }, + windowInsets = WindowInsets(top = 20.dp), // 顶部高度(状态栏) + colors = TopAppBarDefaults.topAppBarColors( + containerColor = MaterialTheme.colorScheme.surface, + scrolledContainerColor = MaterialTheme.colorScheme.surface + ) ) } /** * 聊天选项卡的主界面。 - * 包含连接控制、模式切换、消息列表和输入区域。 - * @param modifier 修饰符 - * @param state 当前的 UI 状态 - * @param onToggleDirectMode 切换广播/私聊模式 - * @param onTargetKeyChange 私聊目标公钥改变 - * @param onDraftChange 草稿消息改变 - * @param onSend 发送消息 - * @param onConnect 连接服务器 - * @param onDisconnect 断开连接 - * @param onClearMessages 清空所有消息 - * @param onCopyMessage 复制消息内容 + * 包含模式切换、消息列表和输入区域。 */ @Composable private fun ChatTab( @@ -257,6 +278,9 @@ private fun ChatTab( onCopyMessage: (String) -> Unit ) { val listState = rememberLazyListState() + + // 定义翻译函数 t + fun t(key: String) = LanguageManager.getString(key, state.language) // 当消息列表新增消息时,自动滚动到底部 LaunchedEffect(state.visibleMessages.size) { @@ -269,48 +293,42 @@ private fun ChatTab( modifier = modifier .fillMaxSize() .imePadding() - .padding(horizontal = 16.dp, vertical = 8.dp) + .padding(start = 16.dp, end = 16.dp, bottom = 8.dp) // 移除了顶部多余 padding ) { - // 连接状态卡片 - ConnectionRow( - statusHint = state.statusHint, - canConnect = state.canConnect, - canDisconnect = state.canDisconnect, - onConnect = onConnect, - onDisconnect = onDisconnect, - onClearMessages = onClearMessages - ) - - Spacer(modifier = Modifier.height(8.dp)) - // 广播/私聊模式切换按钮 - Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(12.dp), + verticalAlignment = Alignment.CenterVertically + ) { FilterChip( selected = !state.directMode, onClick = { onToggleDirectMode(false) }, - label = { Text("广播") } + label = { Text(t("chat.broadcast")) } ) FilterChip( selected = state.directMode, onClick = { onToggleDirectMode(true) }, - label = { Text("私聊") } + label = { Text(t("chat.private")) } ) + + // 在这一行腾出的空间可以放置其他快捷操作,或者保持简洁 } // 私聊模式下显示目标公钥输入框 if (state.directMode) { - Spacer(modifier = Modifier.height(8.dp)) + Spacer(modifier = Modifier.height(4.dp)) OutlinedTextField( value = state.targetKey, onValueChange = onTargetKeyChange, modifier = Modifier.fillMaxWidth(), - label = { Text("目标公钥") }, - placeholder = { Text("私聊模式:粘贴目标公钥") }, + label = { Text(t("chat.target_key")) }, + placeholder = { Text(t("chat.target_key")) }, maxLines = 3 ) } - Spacer(modifier = Modifier.height(8.dp)) + Spacer(modifier = Modifier.height(4.dp)) // 消息列表 LazyColumn( @@ -318,7 +336,7 @@ private fun ChatTab( .weight(1f) .fillMaxWidth(), state = listState, - contentPadding = PaddingValues(vertical = 8.dp), + contentPadding = PaddingValues(vertical = 4.dp), verticalArrangement = Arrangement.spacedBy(8.dp) ) { if (state.visibleMessages.isEmpty()) { @@ -330,7 +348,7 @@ private fun ChatTab( ) ) { Text( - text = "连接后即可聊天。默认广播,切换到私聊后可填写目标公钥。", + text = t("chat.empty_hint"), modifier = Modifier.padding(12.dp), style = MaterialTheme.typography.bodyMedium ) @@ -340,7 +358,8 @@ private fun ChatTab( items(state.visibleMessages, key = { it.id }) { message -> MessageItem( message = message, - onCopy = { onCopyMessage(message.content) } + onCopy = { onCopyMessage(message.content) }, + currentLanguage = state.language ) } } @@ -357,7 +376,7 @@ private fun ChatTab( value = state.draft, onValueChange = onDraftChange, modifier = Modifier.weight(1f), - label = { Text("输入消息") }, + label = { Text(t("chat.input_placeholder")) }, maxLines = 4, keyboardOptions = KeyboardOptions(imeAction = ImeAction.Send), keyboardActions = KeyboardActions( @@ -370,56 +389,9 @@ private fun ChatTab( enabled = state.canSend, modifier = Modifier.height(56.dp) ) { - Icon(Icons.AutoMirrored.Rounded.Send, contentDescription = "发送") + Icon(Icons.AutoMirrored.Rounded.Send, contentDescription = null) Spacer(Modifier.width(6.dp)) - Text(if (state.sending) "发送中" else "发送") - } - } - } -} - -/** - * 连接状态控制卡片,显示当前状态提示并提供连接/断开/清空按钮。 - * @param statusHint 详细的状态提示文本 - * @param canConnect 是否可连接 - * @param canDisconnect 是否可断开 - * @param onConnect 连接回调 - * @param onDisconnect 断开回调 - * @param onClearMessages 清空消息回调 - */ -@Composable -private fun ConnectionRow( - statusHint: String, - canConnect: Boolean, - canDisconnect: Boolean, - onConnect: () -> Unit, - onDisconnect: () -> Unit, - onClearMessages: () -> Unit -) { - Card( - modifier = Modifier.fillMaxWidth(), - colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant) - ) { - Column(modifier = Modifier.padding(12.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) { - Text( - text = "在线会话", - style = MaterialTheme.typography.titleMedium - ) - Text( - text = statusHint, - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { - Button(onClick = onConnect, enabled = canConnect) { - Text("连接") - } - OutlinedButton(onClick = onDisconnect, enabled = canDisconnect) { - Text("断开") - } - OutlinedButton(onClick = onClearMessages) { - Text("清空") - } + Text(if (state.sending) "..." else t("chat.send")) } } } @@ -427,144 +399,146 @@ private fun ConnectionRow( /** * 单个消息气泡组件。 - * 根据消息角色(系统、发出、接收)显示不同的样式。 - * @param message 要显示的消息数据 - * @param onCopy 复制消息内容的回调 */ +@SuppressLint("UnusedBoxWithConstraintsScope") @Composable private fun MessageItem( message: UiMessage, - onCopy: () -> Unit + onCopy: () -> Unit, + currentLanguage: String ) { - BoxWithConstraints(modifier = Modifier.fillMaxWidth()) { - val maxBubbleWidth = if (message.role == MessageRole.SYSTEM) { - maxWidth * 0.9f - } else { - maxWidth * 0.82f - } + Box(modifier = Modifier.fillMaxWidth()) { + val isSystem = message.role == MessageRole.SYSTEM - // 系统消息居中显示 - if (message.role == MessageRole.SYSTEM) { - Card( + if (isSystem) { + // 系统消息居中显示,最大占比 90% + Box( modifier = Modifier - .widthIn(max = maxBubbleWidth) + .fillMaxWidth(0.9f) .align(Alignment.Center), - shape = RoundedCornerShape(14.dp), - colors = CardDefaults.cardColors( - containerColor = MaterialTheme.colorScheme.secondaryContainer - ) + contentAlignment = Alignment.Center ) { - Row( - modifier = Modifier.padding(horizontal = 10.dp, vertical = 8.dp), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(8.dp) - ) { - Text( - text = message.content, - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSecondaryContainer - ) - Text( - text = formatTime(message.timestampMillis), - style = MaterialTheme.typography.labelSmall, - color = MaterialTheme.colorScheme.onSecondaryContainer.copy(alpha = 0.7f) + Card( + shape = RoundedCornerShape(14.dp), + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.secondaryContainer ) + ) { + Row( + modifier = Modifier.padding(horizontal = 10.dp, vertical = 8.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + Text( + text = message.content, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSecondaryContainer + ) + Text( + text = formatTime(message.timestampMillis), + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.onSecondaryContainer.copy(alpha = 0.7f) + ) + } } } - return@BoxWithConstraints - } - - val isOutgoing = message.role == MessageRole.OUTGOING - val bubbleColor = if (isOutgoing) { - MaterialTheme.colorScheme.primaryContainer - } else { - MaterialTheme.colorScheme.surfaceContainer - } - val bubbleTextColor = if (isOutgoing) { - MaterialTheme.colorScheme.onPrimaryContainer } else { - MaterialTheme.colorScheme.onSurface - } - val bubbleShape = if (isOutgoing) { - RoundedCornerShape( - topStart = 18.dp, - topEnd = 6.dp, - bottomEnd = 18.dp, - bottomStart = 18.dp - ) - } else { - RoundedCornerShape( - topStart = 6.dp, - topEnd = 18.dp, - bottomEnd = 18.dp, - bottomStart = 18.dp - ) - } + val isOutgoing = message.role == MessageRole.OUTGOING + val bubbleColor = if (isOutgoing) { + MaterialTheme.colorScheme.primaryContainer + } else { + MaterialTheme.colorScheme.surfaceContainer + } + val bubbleTextColor = if (isOutgoing) { + MaterialTheme.colorScheme.onPrimaryContainer + } else { + MaterialTheme.colorScheme.onSurface + } + val bubbleShape = if (isOutgoing) { + RoundedCornerShape( + topStart = 18.dp, + topEnd = 6.dp, + bottomEnd = 18.dp, + bottomStart = 18.dp + ) + } else { + RoundedCornerShape( + topStart = 6.dp, + topEnd = 18.dp, + bottomEnd = 18.dp, + bottomStart = 18.dp + ) + } - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = if (isOutgoing) Arrangement.End else Arrangement.Start - ) { - Card( - modifier = Modifier.widthIn(max = maxBubbleWidth), - shape = bubbleShape, - colors = CardDefaults.cardColors(containerColor = bubbleColor) + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = if (isOutgoing) Arrangement.End else Arrangement.Start ) { - Column( - modifier = Modifier.padding(horizontal = 12.dp, vertical = 8.dp), - verticalArrangement = Arrangement.spacedBy(6.dp) + // 使用 Box(fillMaxWidth(0.82f)) 限制气泡最大宽度占比 + Box( + modifier = Modifier.fillMaxWidth(0.82f), + contentAlignment = if (isOutgoing) Alignment.CenterEnd else Alignment.CenterStart ) { - // 接收消息时显示发送者信息 - if (!isOutgoing) { - Row(verticalAlignment = Alignment.CenterVertically) { + Card( + shape = bubbleShape, + colors = CardDefaults.cardColors(containerColor = bubbleColor) + ) { + Column( + modifier = Modifier.padding(horizontal = 12.dp, vertical = 8.dp), + verticalArrangement = Arrangement.spacedBy(6.dp) + ) { + if (!isOutgoing) { + Row(verticalAlignment = Alignment.CenterVertically) { + Text( + text = message.sender, + style = MaterialTheme.typography.labelLarge, + color = MaterialTheme.colorScheme.primary + ) + if (message.subtitle.isNotBlank()) { + Spacer(modifier = Modifier.width(8.dp)) + Text( + text = message.subtitle, + style = MaterialTheme.typography.labelSmall, + color = bubbleTextColor.copy(alpha = 0.75f), + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } + } + } + + // 消息正文 Text( - text = message.sender, - style = MaterialTheme.typography.labelLarge, - color = MaterialTheme.colorScheme.primary + text = message.content, + style = MaterialTheme.typography.bodyMedium, + color = bubbleTextColor ) - if (message.subtitle.isNotBlank()) { - Spacer(modifier = Modifier.width(8.dp)) + + // 时间戳和复制按钮 + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically + ) { + Spacer(modifier = Modifier.weight(1f)) Text( - text = message.subtitle, + text = formatTime(message.timestampMillis), style = MaterialTheme.typography.labelSmall, - color = bubbleTextColor.copy(alpha = 0.75f), - maxLines = 1, - overflow = TextOverflow.Ellipsis + color = bubbleTextColor.copy(alpha = 0.7f) ) + IconButton( + onClick = onCopy, + modifier = Modifier.size(24.dp) + ) { + Icon( + imageVector = Icons.Rounded.ContentCopy, + contentDescription = LanguageManager.getString("common.copied", currentLanguage), + tint = bubbleTextColor.copy(alpha = 0.7f), + modifier = Modifier.size(14.dp) + ) + } } } } - - // 消息正文 - Text( - text = message.content, - style = MaterialTheme.typography.bodyMedium, - color = bubbleTextColor - ) - - // 时间戳和复制按钮 - Row( - modifier = Modifier.fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically - ) { - Spacer(modifier = Modifier.weight(1f)) - Text( - text = formatTime(message.timestampMillis), - style = MaterialTheme.typography.labelSmall, - color = bubbleTextColor.copy(alpha = 0.7f) - ) - IconButton( - onClick = onCopy, - modifier = Modifier.size(24.dp) - ) { - Icon( - imageVector = Icons.Rounded.ContentCopy, - contentDescription = "复制", - tint = bubbleTextColor.copy(alpha = 0.7f), - modifier = Modifier.size(14.dp) - ) - } - } } } } @@ -572,7 +546,7 @@ private fun MessageItem( } /** - * 设置选项卡界面,包含个人设置、服务器管理、身份安全和诊断信息。 + * 设置选项卡界面,包含个人设置、服务器管理、身份安全、语言、主题和诊断信息。 * @param modifier 修饰符 * @param state 当前的 UI 状态 * @param onDisplayNameChange 显示名称变更 @@ -586,6 +560,9 @@ private fun MessageItem( * @param onConnect 连接服务器 * @param onDisconnect 断开连接 * @param onClearMessages 清空消息 + * @param onThemeChange 切换主题 + * @param onUseDynamicColorChange 切换动态颜色 + * @param onLanguageChange 切换语言 */ @Composable private fun SettingsTab( @@ -603,84 +580,61 @@ private fun SettingsTab( onDisconnect: () -> Unit, onClearMessages: () -> Unit, onThemeChange: (String) -> Unit, - onUseDynamicColorChange: (Boolean) -> Unit + onUseDynamicColorChange: (Boolean) -> Unit, + onLanguageChange: (String) -> Unit ) { + // 定义翻译函数 t + fun t(key: String) = LanguageManager.getString(key, state.language) + LazyColumn( modifier = modifier .fillMaxSize() .padding(horizontal = 16.dp, vertical = 8.dp), verticalArrangement = Arrangement.spacedBy(12.dp) ) { - // 个人设置卡片 + // 个人设置 item { - Card { - Column( - modifier = Modifier.padding(12.dp), - verticalArrangement = Arrangement.spacedBy(8.dp) - ) { - Text("个人设置", style = MaterialTheme.typography.titleMedium) + Card(modifier = Modifier.fillMaxWidth()) { + Column(modifier = Modifier.padding(12.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) { + Text(t("settings.personal"), style = MaterialTheme.typography.titleMedium) OutlinedTextField( value = state.displayName, onValueChange = onDisplayNameChange, modifier = Modifier.fillMaxWidth(), - label = { Text("显示名称") }, - supportingText = { Text("最长 64 字符") }, + label = { Text(t("settings.display_name")) }, maxLines = 1 ) Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { - Button(onClick = onConnect, enabled = state.canConnect) { - Text("连接") - } - OutlinedButton(onClick = onDisconnect, enabled = state.canDisconnect) { - Text("断开") - } - OutlinedButton(onClick = onClearMessages) { - Text("清空消息") - } + Button(onClick = onConnect, enabled = state.canConnect) { Text(t("settings.connect")) } + OutlinedButton(onClick = onDisconnect, enabled = state.canDisconnect) { Text(t("settings.disconnect")) } + OutlinedButton(onClick = onClearMessages) { Text(t("settings.clear_msg")) } } } } } - // 服务器管理卡片 + // 服务器管理 item { - Card { - Column( - modifier = Modifier.padding(12.dp), - verticalArrangement = Arrangement.spacedBy(8.dp) - ) { - Text("服务器", style = MaterialTheme.typography.titleMedium) + Card(modifier = Modifier.fillMaxWidth()) { + Column(modifier = Modifier.padding(12.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) { + Text(t("settings.server"), style = MaterialTheme.typography.titleMedium) OutlinedTextField( value = state.serverUrl, onValueChange = onServerUrlChange, modifier = Modifier.fillMaxWidth(), - label = { Text("服务器地址") }, - placeholder = { Text("ws://10.0.2.2:13173/") }, + label = { Text(t("settings.server_url")) }, maxLines = 1 ) Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { - Button(onClick = onSaveServer) { - Text("保存地址") - } - OutlinedButton(onClick = onRemoveServer) { - Text("删除当前") - } + Button(onClick = onSaveServer) { Text(t("settings.save_server")) } + OutlinedButton(onClick = onRemoveServer) { Text(t("settings.remove_current")) } } if (state.serverUrls.isNotEmpty()) { HorizontalDivider() - Text("已保存地址", style = MaterialTheme.typography.labelLarge) + Text(t("settings.saved_servers"), style = MaterialTheme.typography.labelLarge) LazyRow(horizontalArrangement = Arrangement.spacedBy(8.dp)) { items(state.serverUrls) { url -> - AssistChip( - onClick = { onSelectServer(url) }, - label = { - Text( - text = url, - maxLines = 1, - overflow = TextOverflow.Ellipsis - ) - } - ) + AssistChip(onClick = { onSelectServer(url) }, label = { Text(url, maxLines = 1, overflow = TextOverflow.Ellipsis) }) } } } @@ -688,80 +642,66 @@ private fun SettingsTab( } } - // 身份与安全卡片 + // 身份与安全 item { - Card { - Column( - modifier = Modifier.padding(12.dp), - verticalArrangement = Arrangement.spacedBy(8.dp) - ) { - Text("身份与安全", style = MaterialTheme.typography.titleMedium) + Card(modifier = Modifier.fillMaxWidth()) { + Column(modifier = Modifier.padding(12.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) { + Text(t("settings.identity"), style = MaterialTheme.typography.titleMedium) Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { - Button( - onClick = onRevealPublicKey, - enabled = !state.loadingPublicKey - ) { - Text(if (state.loadingPublicKey) "读取中" else "查看/生成公钥") - } - OutlinedButton( - onClick = onCopyPublicKey, - enabled = state.myPublicKey.isNotBlank() - ) { - Text("复制公钥") - } + Button(onClick = onRevealPublicKey, enabled = !state.loadingPublicKey) { Text(if (state.loadingPublicKey) "..." else t("settings.reveal_key")) } + OutlinedButton(onClick = onCopyPublicKey, enabled = state.myPublicKey.isNotBlank()) { Text(t("settings.copy_key")) } } - OutlinedTextField( - value = state.myPublicKey, - onValueChange = {}, - modifier = Modifier.fillMaxWidth(), - readOnly = true, - label = { Text("我的公钥") }, - placeholder = { Text("点击“查看/生成公钥”") }, - maxLines = 4 - ) + OutlinedTextField(value = state.myPublicKey, onValueChange = {}, modifier = Modifier.fillMaxWidth(), readOnly = true, label = { Text(t("settings.my_key")) }, maxLines = 4) } } } - // 主题设置卡片 + // 语言选择 item { - Card { - Column( - modifier = Modifier.padding(12.dp), - verticalArrangement = Arrangement.spacedBy(8.dp) - ) { - Text("主题", style = MaterialTheme.typography.titleMedium) - - // 动态颜色开关(仅 Android 12+ 显示) - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(8.dp) - ) { - Switch( - checked = state.useDynamicColor, - onCheckedChange = onUseDynamicColorChange + Card(modifier = Modifier.fillMaxWidth()) { + Column(modifier = Modifier.padding(12.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) { + Text(t("settings.language"), style = MaterialTheme.typography.titleMedium) + LazyRow(horizontalArrangement = Arrangement.spacedBy(8.dp)) { + items(LanguageManager.supportedLanguages) { lang -> + FilterChip( + selected = state.language == lang.code, + onClick = { onLanguageChange(lang.code) }, + label = { Text(lang.name) }, + leadingIcon = { Icon(Icons.Rounded.Language, contentDescription = null, modifier = Modifier.size(16.dp)) } ) - Text("使用动态颜色(跟随系统)") } } + } + } + } - // 当动态颜色关闭时,显示预设主题选择 + // 主题设置 + item { + Card(modifier = Modifier.fillMaxWidth()) { + Column(modifier = Modifier.padding(12.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) { + Text(t("settings.theme"), style = MaterialTheme.typography.titleMedium) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp)) { + Switch(checked = state.useDynamicColor, onCheckedChange = onUseDynamicColorChange) + Text(t("settings.dynamic_color")) + } + } if (!state.useDynamicColor || Build.VERSION.SDK_INT < Build.VERSION_CODES.S) { - Text("预设主题", style = MaterialTheme.typography.labelLarge) + Text(t("settings.preset_themes"), style = MaterialTheme.typography.labelLarge) LazyRow(horizontalArrangement = Arrangement.spacedBy(8.dp)) { items(themeOptions) { option -> + val themeName = when(option.id) { + "blue" -> t("theme.blue") + "gray" -> t("theme.gray") + "green" -> t("theme.green") + "red" -> t("theme.red") + else -> option.name + } FilterChip( selected = state.themeId == option.id, onClick = { onThemeChange(option.id) }, - label = { Text(option.name) }, - leadingIcon = { - Box( - modifier = Modifier - .size(16.dp) - .background(option.primary, RoundedCornerShape(4.dp)) - ) - } + label = { Text(themeName) }, + leadingIcon = { Box(modifier = Modifier.size(16.dp).background(option.primary, RoundedCornerShape(4.dp))) } ) } } @@ -770,34 +710,23 @@ private fun SettingsTab( } } - // 诊断信息卡片 + // 诊断信息 item { - Card { - Column( - modifier = Modifier.padding(12.dp), - verticalArrangement = Arrangement.spacedBy(8.dp) - ) { - Text("诊断", style = MaterialTheme.typography.titleMedium) - Text("连接提示:${state.statusHint}") - Text("当前状态:${state.statusText}") - Text("证书指纹:${state.certFingerprint.ifBlank { "未获取" }}") - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(8.dp) - ) { - Switch( - checked = state.showSystemMessages, - onCheckedChange = onToggleShowSystem - ) - Text("显示系统消息") + Card(modifier = Modifier.fillMaxWidth()) { + Column(modifier = Modifier.padding(12.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) { + Text(t("settings.diagnostics"), style = MaterialTheme.typography.titleMedium) + Text("${t("settings.status_hint")}:${state.statusHint}") + Text("${t("settings.current_status")}:${state.statusText}") + Text("${t("settings.cert_fingerprint")}:${state.certFingerprint.ifBlank { "N/A" }}") + Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp)) { + Switch(checked = state.showSystemMessages, onCheckedChange = onToggleShowSystem) + Text(t("settings.show_system")) } } } } - item { - Spacer(modifier = Modifier.height(12.dp)) - } + item { Spacer(modifier = Modifier.height(12.dp)) } } } @@ -812,4 +741,4 @@ private fun formatTime(tsMillis: Long): String { .atZone(ZoneId.systemDefault()) .toLocalTime() .format(formatter) -} \ No newline at end of file +} diff --git a/android-client/app/src/main/java/com/onlinemsg/client/ui/ChatSessionManager.kt b/android-client/app/src/main/java/com/onlinemsg/client/ui/ChatSessionManager.kt index f8ce61d..b9a1f25 100644 --- a/android-client/app/src/main/java/com/onlinemsg/client/ui/ChatSessionManager.kt +++ b/android-client/app/src/main/java/com/onlinemsg/client/ui/ChatSessionManager.kt @@ -183,8 +183,9 @@ object ChatSessionManager { serverUrl = pref.currentServerUrl, directMode = pref.directMode, showSystemMessages = pref.showSystemMessages, - themeId = pref.themeId, // 假设 preferences 中有 themeId - useDynamicColor = pref.useDynamicColor + themeId = pref.themeId, + useDynamicColor = pref.useDynamicColor, + language = pref.language ) } // 如果上次会话启用了自动重连,则自动恢复连接 @@ -207,6 +208,17 @@ object ChatSessionManager { } } + /** + * 更新语言 + * @param language 语言代码 + */ + fun updateLanguage(language: String) { + _uiState.update { it.copy(language = language) } + scope.launch { + preferencesRepository.setLanguage(language) + } + } + /** * 更改使用动态颜色 * @param enabled 主题名 @@ -519,7 +531,7 @@ object ChatSessionManager { } /** - * 确保本地身份已加载或创建。 + * 确保本地身份已加载 or 创建。 * @return 本地身份对象 */ private suspend fun ensureIdentity(): RsaCryptoManager.Identity { @@ -533,7 +545,7 @@ object ChatSessionManager { } /** - * 处理收到的原始文本消息(可能是握手包或加密消息)。 + * 处理收到的原始文本消息(可能是握手包 or 加密消息)。 * @param rawText 原始文本 */ private suspend fun handleIncomingMessage(rawText: String) { diff --git a/android-client/app/src/main/java/com/onlinemsg/client/ui/ChatUiState.kt b/android-client/app/src/main/java/com/onlinemsg/client/ui/ChatUiState.kt index 88ecb88..c7cc58b 100644 --- a/android-client/app/src/main/java/com/onlinemsg/client/ui/ChatUiState.kt +++ b/android-client/app/src/main/java/com/onlinemsg/client/ui/ChatUiState.kt @@ -67,6 +67,7 @@ data class UiMessage( * @property myPublicKey 本地公钥 * @property sending 是否正在发送消息(用于禁用按钮) * @property loadingPublicKey 是否正在加载公钥 + * @property language 当前选择的语言代码 (如 "zh", "en", "ja") */ data class ChatUiState( val status: ConnectionStatus = ConnectionStatus.IDLE, @@ -83,8 +84,9 @@ data class ChatUiState( val myPublicKey: String = "", val sending: Boolean = false, val loadingPublicKey: Boolean = false, - val themeId: String = "default", /* 当前选中的主题名 (@emilia-t)*/ - val useDynamicColor: Boolean = true /* 是否使用 Android 12+ 动态颜色 (@emilia-t)*/ + val themeId: String = "blue", + val useDynamicColor: Boolean = true, + val language: String = "zh" ) { /** * 是否允许连接。 diff --git a/android-client/app/src/main/java/com/onlinemsg/client/ui/ChatViewModel.kt b/android-client/app/src/main/java/com/onlinemsg/client/ui/ChatViewModel.kt index d6b55ba..8b53474 100644 --- a/android-client/app/src/main/java/com/onlinemsg/client/ui/ChatViewModel.kt +++ b/android-client/app/src/main/java/com/onlinemsg/client/ui/ChatViewModel.kt @@ -5,7 +5,7 @@ import androidx.lifecycle.AndroidViewModel /** * ViewModel 层,作为 UI 与 [ChatSessionManager] 的桥梁。 - * 初始化会话管理器并暴露其状态和事件流,同时提供所有用户操作的代理方法。 + * 初始化会话管理器并暴露其状态 and 事件流,同时提供所有用户操作的代理方法。 * @param application Application 实例 */ class ChatViewModel(application: Application) : AndroidViewModel(application) { @@ -34,4 +34,5 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) { fun updateTheme(themeId: String) = ChatSessionManager.updateTheme(themeId) fun updateUseDynamicColor(enabled: Boolean) = ChatSessionManager.updateUseDynamicColor(enabled) + fun updateLanguage(language: String) = ChatSessionManager.updateLanguage(language) } \ No newline at end of file diff --git a/android-client/app/src/main/java/com/onlinemsg/client/ui/theme/Color.kt b/android-client/app/src/main/java/com/onlinemsg/client/ui/theme/Color.kt index f73b859..97138d3 100644 --- a/android-client/app/src/main/java/com/onlinemsg/client/ui/theme/Color.kt +++ b/android-client/app/src/main/java/com/onlinemsg/client/ui/theme/Color.kt @@ -2,10 +2,16 @@ package com.onlinemsg.client.ui.theme import androidx.compose.ui.graphics.Color -// 应用的主色调、表面色、错误色等定义 -val Primary = Color(0xFF0C6D62) +// 浅色模式 - 温暖色调定义 +val Primary = Color(0xFFD84315) // 深橙色 800 val OnPrimary = Color(0xFFFFFFFF) -val Secondary = Color(0xFF4A635F) -val Surface = Color(0xFFF7FAF8) -val SurfaceVariant = Color(0xFFDCE8E4) -val Error = Color(0xFFB3261E) \ No newline at end of file +val Secondary = Color(0xFF6D4C41) // 棕色 600 +val Surface = Color(0xFFFFFBF0) // 温暖的奶油白 +val SurfaceVariant = Color(0xFFFBE9E7) // 极浅橙色 +val Error = Color(0xFFB00020) + +// 深色模式 - 温暖色调定义 +val DarkPrimary = Color(0xFFFFB5A0) // 浅暖橙 +val DarkSecondary = Color(0xFFD7C1B1) // 浅棕 +val DarkSurface = Color(0xFF1D1B16) // 暖深褐色(解决深色模式下背景过亮问题) +val OnDarkSurface = Color(0xFFEDE0DD) diff --git a/android-client/app/src/main/java/com/onlinemsg/client/ui/theme/Theme.kt b/android-client/app/src/main/java/com/onlinemsg/client/ui/theme/Theme.kt index 2843bee..2e6c09a 100644 --- a/android-client/app/src/main/java/com/onlinemsg/client/ui/theme/Theme.kt +++ b/android-client/app/src/main/java/com/onlinemsg/client/ui/theme/Theme.kt @@ -21,8 +21,11 @@ private val LightColors = lightColorScheme( ) private val DarkColors = darkColorScheme( - primary = Primary.copy(alpha = 0.9f), - secondary = Secondary.copy(alpha = 0.9f), + primary = DarkPrimary, + onPrimary = Color.Black, + secondary = DarkSecondary, + surface = DarkSurface, + onSurface = OnDarkSurface, error = Error ) @@ -30,40 +33,39 @@ private val DarkColors = darkColorScheme( * 应用主题可组合函数。 * 支持浅色/深色模式以及 Android 12+ 的动态颜色。 * @param darkTheme 是否强制深色模式(默认跟随系统) - * @param dynamicColor 是否启用动态颜色(默认 true) - * @param content 内部内容 + * @param themeId 当前选中的主题 ID (默认为 "blue") + * @param useDynamicColor 是否启用动态颜色(Android 12+ 支持) */ @Composable fun OnlineMsgTheme( darkTheme: Boolean = isSystemInDarkTheme(), - themeId: String = "default", // 从外部传入 - useDynamicColor: Boolean = true, // 从外部传入 + themeId: String = "blue", // 默认预设设为 blue + useDynamicColor: Boolean = true, content: @Composable () -> Unit ) { val context = LocalContext.current val colorScheme = when { - // 优先使用动态颜色(如果启用且系统支持) + // 1. 优先使用动态颜色(如果启用且系统支持) useDynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) } - // 否则根据主题 ID 选择预设 + // 2. 根据 themeId 选择预设 else -> { val option = themeOptions.find { it.id == themeId } ?: themeOptions.first() if (darkTheme) { darkColorScheme( - primary = option.primary, - secondary = option.secondary, - error = option.error ?: Error, - surface = option.surface ?: Surface, - // 其他颜色可以沿用默认深色调整 + primary = option.primaryDark ?: option.primary.copy(alpha = 0.8f), + secondary = option.secondary.copy(alpha = 0.8f), + surface = option.surfaceDark ?: DarkSurface, + onSurface = OnDarkSurface, + error = option.error ?: Error ) } else { lightColorScheme( primary = option.primary, secondary = option.secondary, - error = option.error ?: Error, surface = option.surface ?: Surface, - // 其他颜色保持默认浅色 + error = option.error ?: Error ) } } @@ -83,13 +85,38 @@ data class ThemeOption( val primary: Color, val secondary: Color, val surface: Color? = null, + val primaryDark: Color? = null, // 显式深色主色 + val surfaceDark: Color? = null, // 显式深色背景 val error: Color? = null ) // 预设主题列表 val themeOptions = listOf( - ThemeOption("default", "默认", Primary, Secondary), - ThemeOption("blue", "蔚蓝", Color(0xFF1E88E5), Color(0xFF6A8DAA)), - ThemeOption("green", "翠绿", Color(0xFF2E7D32), Color(0xFF4A635F)), - ThemeOption("red", "绯红", Color(0xFFC62828), Color(0xFF8D6E63)) -) \ No newline at end of file + // 默认列表首位即为默认主题 + ThemeOption( + id = "blue", + name = "蔚蓝", + primary = Color(0xFF1E88E5), + secondary = Color(0xFF6A8DAA) + ), + ThemeOption( + id = "gray", + name = "商务灰", + primary = Color(0xFF607D8B), + secondary = Color(0xFF90A4AE), + primaryDark = Color(0xFFCFD8DC), + surfaceDark = Color(0xFF263238) + ), + ThemeOption( + id = "green", + name = "翠绿", + primary = Color(0xFF2E7D32), + secondary = Color(0xFF4A635F) + ), + ThemeOption( + id = "red", + name = "绯红", + primary = Color(0xFFC62828), + secondary = Color(0xFF8D6E63) + ) +) diff --git a/android-client/app/src/main/java/com/onlinemsg/client/util/LanguageManager.kt b/android-client/app/src/main/java/com/onlinemsg/client/util/LanguageManager.kt new file mode 100644 index 0000000..355c591 --- /dev/null +++ b/android-client/app/src/main/java/com/onlinemsg/client/util/LanguageManager.kt @@ -0,0 +1,188 @@ +package com.onlinemsg.client.util + +/** + * 语言管理类,统一存储应用内的多语言词条。 + * 类似于 Minecraft 的语言文件映射。 + */ +object LanguageManager { + + private val translations = mapOf( + "zh" to mapOf( + "tab.chat" to "聊天", + "tab.settings" to "设置", + "settings.personal" to "个人设置", + "settings.display_name" to "显示名称", + "settings.server" to "服务器", + "settings.server_url" to "服务器地址", + "settings.save_server" to "保存地址", + "settings.remove_current" to "删除当前", + "settings.saved_servers" to "已保存地址", + "settings.identity" to "身份与安全", + "settings.reveal_key" to "查看/生成公钥", + "settings.copy_key" to "复制公钥", + "settings.my_key" to "我的公钥", + "settings.theme" to "主题", + "settings.preset_themes" to "预设主题", + "settings.language" to "语言", + "settings.diagnostics" to "诊断", + "settings.status_hint" to "连接提示", + "settings.current_status" to "当前状态", + "settings.cert_fingerprint" to "证书指纹", + "settings.show_system" to "显示系统消息", + "settings.connect" to "连接", + "settings.disconnect" to "断开", + "settings.clear_msg" to "清空消息", + "settings.dynamic_color" to "使用动态颜色", + "chat.broadcast" to "广播", + "chat.private" to "私聊", + "chat.target_key" to "目标公钥", + "chat.input_placeholder" to "输入消息", + "chat.send" to "发送", + "chat.sending" to "发送中", + "chat.empty_hint" to "连接后即可聊天。默认广播,切换到私聊后可填写目标公钥。", + "common.copied" to "已复制", + "common.unknown" to "未知", + "theme.blue" to "蔚蓝", + "theme.gray" to "商务灰", + "theme.green" to "翠绿", + "theme.red" to "绯红", + "theme.warm" to "温暖" + ), + "en" to mapOf( + "tab.chat" to "Chat", + "tab.settings" to "Settings", + "settings.personal" to "Personal", + "settings.display_name" to "Display Name", + "settings.server" to "Server", + "settings.server_url" to "Server Address", + "settings.save_server" to "Save", + "settings.remove_current" to "Remove", + "settings.saved_servers" to "Saved Addresses", + "settings.identity" to "Identity & Security", + "settings.reveal_key" to "Reveal/Generate Key", + "settings.copy_key" to "Copy Key", + "settings.my_key" to "My Public Key", + "settings.theme" to "Theme", + "settings.preset_themes" to "Preset Themes", + "settings.language" to "Language", + "settings.diagnostics" to "Diagnostics", + "settings.status_hint" to "Hint", + "settings.current_status" to "Status", + "settings.cert_fingerprint" to "Fingerprint", + "settings.show_system" to "Show System Messages", + "settings.connect" to "Link", + "settings.disconnect" to "Dislink", + "settings.clear_msg" to "ClearMsg", + "settings.dynamic_color" to "Use dynamic color", + "chat.broadcast" to "Broadcast", + "chat.private" to "Private", + "chat.target_key" to "Target Public Key", + "chat.input_placeholder" to "Type a message", + "chat.send" to "Send", + "chat.sending" to "Sending", + "chat.empty_hint" to "Connect to start chatting. Default is broadcast.", + "common.copied" to "Copied", + "common.unknown" to "Unknown", + "theme.blue" to "Blue", + "theme.gray" to "Business Gray", + "theme.green" to "Green", + "theme.red" to "Red", + "theme.warm" to "Warm" + ), + "ja" to mapOf( + "tab.chat" to "チャット", + "tab.settings" to "設定", + "settings.personal" to "個人設定", + "settings.display_name" to "表示名", + "settings.server" to "サーバー", + "settings.server_url" to "アドレス", + "settings.save_server" to "保存", + "settings.remove_current" to "削除", + "settings.saved_servers" to "保存済みアドレス", + "settings.identity" to "セキュリティ", + "settings.reveal_key" to "公開鍵を表示/生成", + "settings.copy_key" to "鍵をコピー", + "settings.my_key" to "マイ公開鍵", + "settings.theme" to "テーマ", + "settings.preset_themes" to "プリセット", + "settings.language" to "言語", + "settings.diagnostics" to "診断", + "settings.status_hint" to "ヒント", + "settings.current_status" to "ステータス", + "settings.cert_fingerprint" to "証明書指紋", + "settings.show_system" to "システムメッセージを表示", + "settings.connect" to "接続", + "settings.disconnect" to "切断", + "settings.clear_msg" to "履歴を消去", + "settings.dynamic_color" to "動的カラーを使用", + "chat.broadcast" to "全体", + "chat.private" to "個人", + "chat.target_key" to "相手の公開鍵", + "chat.input_placeholder" to "メッセージを入力", + "chat.send" to "送信", + "chat.sending" to "送信中", + "chat.empty_hint" to "接続するとチャットを開始できます。", + "common.copied" to "コピーしました", + "common.unknown" to "不明", + "theme.blue" to "ブルー", + "theme.gray" to "ビジネスグレー", + "theme.green" to "グリーン", + "theme.red" to "レッド", + "theme.warm" to "ウォーム" + ), + "ko" to mapOf( + "tab.chat" to "채팅", + "tab.settings" to "설정", + "settings.personal" to "개인 설정", + "settings.display_name" to "표시 이름", + "settings.server" to "서버", + "settings.server_url" to "서버 주소", + "settings.save_server" to "주소 저장", + "settings.remove_current" to "현재 삭제", + "settings.saved_servers" to "저장된 주소", + "settings.identity" to "신원 및 보안", + "settings.reveal_key" to "공개키 보기/생성", + "settings.copy_key" to "공개키 복사", + "settings.my_key" to "나의 공개키", + "settings.theme" to "테마", + "settings.preset_themes" to "프리셋 테마", + "settings.language" to "언어", + "settings.diagnostics" to "진단", + "settings.status_hint" to "연결 힌트", + "settings.current_status" to "현재 상태", + "settings.cert_fingerprint" to "인증서 지문", + "settings.show_system" to "시스템 메시지 표시", + "settings.connect" to "연결", + "settings.disconnect" to "연결 끊기", + "settings.clear_msg" to "정보 삭제", + "settings.dynamic_color" to "동적 색상 사용", + "chat.broadcast" to "브로드캐스트", + "chat.private" to "비공개 채팅", + "chat.target_key" to "대상 공개키", + "chat.input_placeholder" to "메시지 입력", + "chat.send" to "전송", + "chat.sending" to "전송 중", + "chat.empty_hint" to "연결 후 채팅이 가능합니다. 기본은 브로드캐스트이며, 비공개 채팅으로 전환 후 대상 공개키를 입력할 수 있습니다.", + "common.copied" to "복사됨", + "common.unknown" to "알 수 없음", + "theme.blue" to "파랑", + "theme.gray" to "비즈니스 그레이", + "theme.green" to "초록", + "theme.red" to "빨강", + "theme.warm" to "따뜻함" + ) + ) + + fun getString(key: String, lang: String): String { + return translations[lang]?.get(key) ?: translations["en"]?.get(key) ?: key + } + + val supportedLanguages = listOf( + LanguageOption("zh", "中文"), + LanguageOption("en", "English"), + LanguageOption("ja", "日本语"), + LanguageOption("ko", "한국어") + ) +} + +data class LanguageOption(val code: String, val name: String)