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)