1修复深色模式显示问题2调整部分UI3增加主题支持4增加多语言支持

pull/2/head
minxiwan 2 weeks ago
parent 865dafc898
commit e9d519554b

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="GradleSettings">
<option name="linkedExternalProjectsSettings">
<GradleProjectSettings>
<option name="testRunner" value="CHOOSE_PER_TEST" />
<option name="externalProjectPath" value="$PROJECT_DIR$/android-client" />
<option name="gradleJvm" value="21" />
</GradleProjectSettings>
</option>
</component>
</project>

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="StudioBotProjectSettings">
<option name="shareContext" value="OptedIn" />
</component>
</project>

@ -13,7 +13,7 @@ android {
minSdk = 26 minSdk = 26
targetSdk = 34 targetSdk = 34
versionCode = 1 versionCode = 1
versionName = "1.0.0.1" versionName = "1.0.0.2"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables { vectorDrawables {

@ -22,8 +22,9 @@ data class UserPreferences(
val showSystemMessages: Boolean, val showSystemMessages: Boolean,
val directMode: Boolean, val directMode: Boolean,
val shouldAutoReconnect: Boolean, val shouldAutoReconnect: Boolean,
val themeId: String = "default", val themeId: String = "blue",
val useDynamicColor: Boolean = true val useDynamicColor: Boolean = true,
val language: String = "zh" // 默认中文
) )
class UserPreferencesRepository( class UserPreferencesRepository(
@ -47,8 +48,9 @@ class UserPreferencesRepository(
showSystemMessages = prefs[KEY_SHOW_SYSTEM_MESSAGES] ?: false, showSystemMessages = prefs[KEY_SHOW_SYSTEM_MESSAGES] ?: false,
directMode = prefs[KEY_DIRECT_MODE] ?: false, directMode = prefs[KEY_DIRECT_MODE] ?: false,
shouldAutoReconnect = prefs[KEY_SHOULD_AUTO_RECONNECT] ?: false, shouldAutoReconnect = prefs[KEY_SHOULD_AUTO_RECONNECT] ?: false,
themeId = prefs[KEY_THEME_ID] ?: "default", themeId = prefs[KEY_THEME_ID] ?: "blue",
useDynamicColor = prefs[KEY_USE_DYNAMIC_COLOR] ?: true 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) { suspend fun setUseDynamicColor(enabled: Boolean) {
context.dataStore.edit { prefs -> context.dataStore.edit { prefs ->
@ -147,5 +154,6 @@ class UserPreferencesRepository(
val KEY_SHOULD_AUTO_RECONNECT: Preferences.Key<Boolean> = booleanPreferencesKey("should_auto_reconnect") val KEY_SHOULD_AUTO_RECONNECT: Preferences.Key<Boolean> = booleanPreferencesKey("should_auto_reconnect")
val KEY_THEME_ID: Preferences.Key<String> = stringPreferencesKey("theme_id") val KEY_THEME_ID: Preferences.Key<String> = stringPreferencesKey("theme_id")
val KEY_USE_DYNAMIC_COLOR: Preferences.Key<Boolean> = booleanPreferencesKey("use_dynamic_color") val KEY_USE_DYNAMIC_COLOR: Preferences.Key<Boolean> = booleanPreferencesKey("use_dynamic_color")
val KEY_LANGUAGE: Preferences.Key<String> = stringPreferencesKey("language")
} }
} }

@ -1,10 +1,11 @@
package com.onlinemsg.client.ui package com.onlinemsg.client.ui
import android.annotation.SuppressLint
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.consumeWindowInsets import androidx.compose.foundation.layout.consumeWindowInsets
import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.navigationBarsPadding 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.Icons
import androidx.compose.material.icons.automirrored.rounded.Send import androidx.compose.material.icons.automirrored.rounded.Send
import androidx.compose.material.icons.rounded.ContentCopy 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.AssistChip
import androidx.compose.material3.Button import androidx.compose.material3.Button
import androidx.compose.material3.Card import androidx.compose.material3.Card
@ -48,6 +52,7 @@ import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Switch import androidx.compose.material3.Switch
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
@ -72,16 +77,15 @@ import java.time.Instant
import java.time.ZoneId import java.time.ZoneId
import java.time.format.DateTimeFormatter import java.time.format.DateTimeFormatter
import com.onlinemsg.client.ui.theme.themeOptions import com.onlinemsg.client.ui.theme.themeOptions
import com.onlinemsg.client.ui.theme.ThemeOption import com.onlinemsg.client.util.LanguageManager
/** /**
* 主界面底部导航栏的选项卡枚举 * 主界面底部导航栏的选项卡枚举
* @property label 选项卡显示的文本
*/ */
private enum class MainTab(val label: String) { private enum class MainTab(val labelKey: String) {
CHAT("聊天"), CHAT("tab.chat"),
SETTINGS("设置") SETTINGS("tab.settings")
} }
/** /**
@ -91,6 +95,7 @@ private enum class MainTab(val label: String) {
*/ */
@Composable @Composable
fun OnlineMsgApp(viewModel: ChatViewModel = viewModel()) { fun OnlineMsgApp(viewModel: ChatViewModel = viewModel()) {
val state by viewModel.uiState.collectAsStateWithLifecycle() val state by viewModel.uiState.collectAsStateWithLifecycle()
OnlineMsgTheme( OnlineMsgTheme(
darkTheme = isSystemInDarkTheme(), // 仍可跟随系统 darkTheme = isSystemInDarkTheme(), // 仍可跟随系统
@ -98,11 +103,13 @@ fun OnlineMsgApp(viewModel: ChatViewModel = viewModel()) {
useDynamicColor = state.useDynamicColor useDynamicColor = state.useDynamicColor
) )
{ {
val state by viewModel.uiState.collectAsStateWithLifecycle()
val clipboard = LocalClipboardManager.current val clipboard = LocalClipboardManager.current
val snackbarHostState = remember { SnackbarHostState() } val snackbarHostState = remember { SnackbarHostState() }
var tab by rememberSaveable { mutableStateOf(MainTab.CHAT) } var tab by rememberSaveable { mutableStateOf(MainTab.CHAT) }
// 定义翻译函数 t
fun t(key: String) = LanguageManager.getString(key, state.language)
// 监听 ViewModel 发送的 UI 事件(如 Snackbar 消息) // 监听 ViewModel 发送的 UI 事件(如 Snackbar 消息)
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
viewModel.events.collect { event -> viewModel.events.collect { event ->
@ -124,18 +131,36 @@ fun OnlineMsgApp(viewModel: ChatViewModel = viewModel()) {
) )
}, },
bottomBar = { bottomBar = {
NavigationBar(modifier = Modifier.navigationBarsPadding()) { NavigationBar(
modifier = Modifier
.navigationBarsPadding()
.height(64.dp),
containerColor = MaterialTheme.colorScheme.surface,
tonalElevation = 0.dp
) {
NavigationBarItem( NavigationBarItem(
selected = tab == MainTab.CHAT, selected = tab == MainTab.CHAT,
onClick = { tab = MainTab.CHAT }, onClick = { tab = MainTab.CHAT },
label = { Text(MainTab.CHAT.label) }, label = { Text(t(MainTab.CHAT.labelKey), style = MaterialTheme.typography.labelSmall) },
icon = {} icon = {
Icon(
imageVector = Icons.Rounded.Forum,
contentDescription = null,
modifier = Modifier.size(22.dp)
)
}
) )
NavigationBarItem( NavigationBarItem(
selected = tab == MainTab.SETTINGS, selected = tab == MainTab.SETTINGS,
onClick = { tab = MainTab.SETTINGS }, onClick = { tab = MainTab.SETTINGS },
label = { Text(MainTab.SETTINGS.label) }, label = { Text(t(MainTab.SETTINGS.labelKey), style = MaterialTheme.typography.labelSmall) },
icon = {} 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, onDisconnect = viewModel::disconnect,
onClearMessages = viewModel::clearMessages, onClearMessages = viewModel::clearMessages,
onThemeChange = viewModel::updateTheme, 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 状态指示点的颜色 * @param statusColor 状态指示点的颜色
*/ */
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@ -207,8 +233,8 @@ private fun AppTopBar(
TopAppBar( TopAppBar(
title = { title = {
Text( Text(
text = "OnlineMsg Chat", text = "OnlineMsg",
style = MaterialTheme.typography.titleLarge style = MaterialTheme.typography.titleMedium
) )
}, },
actions = { actions = {
@ -218,30 +244,25 @@ private fun AppTopBar(
leadingIcon = { leadingIcon = {
Box( Box(
modifier = Modifier modifier = Modifier
.width(10.dp) .size(8.dp)
.height(10.dp)
.background(statusColor, RoundedCornerShape(999.dp)) .background(statusColor, RoundedCornerShape(999.dp))
) )
} },
modifier = Modifier.height(32.dp)
) )
Spacer(modifier = Modifier.width(12.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 @Composable
private fun ChatTab( private fun ChatTab(
@ -258,6 +279,9 @@ private fun ChatTab(
) { ) {
val listState = rememberLazyListState() val listState = rememberLazyListState()
// 定义翻译函数 t
fun t(key: String) = LanguageManager.getString(key, state.language)
// 当消息列表新增消息时,自动滚动到底部 // 当消息列表新增消息时,自动滚动到底部
LaunchedEffect(state.visibleMessages.size) { LaunchedEffect(state.visibleMessages.size) {
if (state.visibleMessages.isNotEmpty()) { if (state.visibleMessages.isNotEmpty()) {
@ -269,48 +293,42 @@ private fun ChatTab(
modifier = modifier modifier = modifier
.fillMaxSize() .fillMaxSize()
.imePadding() .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( FilterChip(
selected = !state.directMode, selected = !state.directMode,
onClick = { onToggleDirectMode(false) }, onClick = { onToggleDirectMode(false) },
label = { Text("广播") } label = { Text(t("chat.broadcast")) }
) )
FilterChip( FilterChip(
selected = state.directMode, selected = state.directMode,
onClick = { onToggleDirectMode(true) }, onClick = { onToggleDirectMode(true) },
label = { Text("私聊") } label = { Text(t("chat.private")) }
) )
// 在这一行腾出的空间可以放置其他快捷操作,或者保持简洁
} }
// 私聊模式下显示目标公钥输入框 // 私聊模式下显示目标公钥输入框
if (state.directMode) { if (state.directMode) {
Spacer(modifier = Modifier.height(8.dp)) Spacer(modifier = Modifier.height(4.dp))
OutlinedTextField( OutlinedTextField(
value = state.targetKey, value = state.targetKey,
onValueChange = onTargetKeyChange, onValueChange = onTargetKeyChange,
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
label = { Text("目标公钥") }, label = { Text(t("chat.target_key")) },
placeholder = { Text("私聊模式:粘贴目标公钥") }, placeholder = { Text(t("chat.target_key")) },
maxLines = 3 maxLines = 3
) )
} }
Spacer(modifier = Modifier.height(8.dp)) Spacer(modifier = Modifier.height(4.dp))
// 消息列表 // 消息列表
LazyColumn( LazyColumn(
@ -318,7 +336,7 @@ private fun ChatTab(
.weight(1f) .weight(1f)
.fillMaxWidth(), .fillMaxWidth(),
state = listState, state = listState,
contentPadding = PaddingValues(vertical = 8.dp), contentPadding = PaddingValues(vertical = 4.dp),
verticalArrangement = Arrangement.spacedBy(8.dp) verticalArrangement = Arrangement.spacedBy(8.dp)
) { ) {
if (state.visibleMessages.isEmpty()) { if (state.visibleMessages.isEmpty()) {
@ -330,7 +348,7 @@ private fun ChatTab(
) )
) { ) {
Text( Text(
text = "连接后即可聊天。默认广播,切换到私聊后可填写目标公钥。", text = t("chat.empty_hint"),
modifier = Modifier.padding(12.dp), modifier = Modifier.padding(12.dp),
style = MaterialTheme.typography.bodyMedium style = MaterialTheme.typography.bodyMedium
) )
@ -340,7 +358,8 @@ private fun ChatTab(
items(state.visibleMessages, key = { it.id }) { message -> items(state.visibleMessages, key = { it.id }) { message ->
MessageItem( MessageItem(
message = message, message = message,
onCopy = { onCopyMessage(message.content) } onCopy = { onCopyMessage(message.content) },
currentLanguage = state.language
) )
} }
} }
@ -357,7 +376,7 @@ private fun ChatTab(
value = state.draft, value = state.draft,
onValueChange = onDraftChange, onValueChange = onDraftChange,
modifier = Modifier.weight(1f), modifier = Modifier.weight(1f),
label = { Text("输入消息") }, label = { Text(t("chat.input_placeholder")) },
maxLines = 4, maxLines = 4,
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Send), keyboardOptions = KeyboardOptions(imeAction = ImeAction.Send),
keyboardActions = KeyboardActions( keyboardActions = KeyboardActions(
@ -370,56 +389,9 @@ private fun ChatTab(
enabled = state.canSend, enabled = state.canSend,
modifier = Modifier.height(56.dp) modifier = Modifier.height(56.dp)
) { ) {
Icon(Icons.AutoMirrored.Rounded.Send, contentDescription = "发送") Icon(Icons.AutoMirrored.Rounded.Send, contentDescription = null)
Spacer(Modifier.width(6.dp)) Spacer(Modifier.width(6.dp))
Text(if (state.sending) "发送中" else "发送") Text(if (state.sending) "..." else t("chat.send"))
}
}
}
}
/**
* 连接状态控制卡片显示当前状态提示并提供连接/断开/清空按钮
* @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("清空")
}
} }
} }
} }
@ -427,144 +399,146 @@ private fun ConnectionRow(
/** /**
* 单个消息气泡组件 * 单个消息气泡组件
* 根据消息角色系统发出接收显示不同的样式
* @param message 要显示的消息数据
* @param onCopy 复制消息内容的回调
*/ */
@SuppressLint("UnusedBoxWithConstraintsScope")
@Composable @Composable
private fun MessageItem( private fun MessageItem(
message: UiMessage, message: UiMessage,
onCopy: () -> Unit onCopy: () -> Unit,
currentLanguage: String
) { ) {
BoxWithConstraints(modifier = Modifier.fillMaxWidth()) { Box(modifier = Modifier.fillMaxWidth()) {
val maxBubbleWidth = if (message.role == MessageRole.SYSTEM) { val isSystem = message.role == MessageRole.SYSTEM
maxWidth * 0.9f
} else {
maxWidth * 0.82f
}
// 系统消息居中显示 if (isSystem) {
if (message.role == MessageRole.SYSTEM) { // 系统消息居中显示,最大占比 90%
Card( Box(
modifier = Modifier modifier = Modifier
.widthIn(max = maxBubbleWidth) .fillMaxWidth(0.9f)
.align(Alignment.Center), .align(Alignment.Center),
shape = RoundedCornerShape(14.dp), contentAlignment = Alignment.Center
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.secondaryContainer
)
) { ) {
Row( Card(
modifier = Modifier.padding(horizontal = 10.dp, vertical = 8.dp), shape = RoundedCornerShape(14.dp),
verticalAlignment = Alignment.CenterVertically, colors = CardDefaults.cardColors(
horizontalArrangement = Arrangement.spacedBy(8.dp) containerColor = MaterialTheme.colorScheme.secondaryContainer
) {
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)
) )
) {
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 { } else {
MaterialTheme.colorScheme.onSurface val isOutgoing = message.role == MessageRole.OUTGOING
} val bubbleColor = if (isOutgoing) {
val bubbleShape = if (isOutgoing) { MaterialTheme.colorScheme.primaryContainer
RoundedCornerShape( } else {
topStart = 18.dp, MaterialTheme.colorScheme.surfaceContainer
topEnd = 6.dp, }
bottomEnd = 18.dp, val bubbleTextColor = if (isOutgoing) {
bottomStart = 18.dp MaterialTheme.colorScheme.onPrimaryContainer
) } else {
} else { MaterialTheme.colorScheme.onSurface
RoundedCornerShape( }
topStart = 6.dp, val bubbleShape = if (isOutgoing) {
topEnd = 18.dp, RoundedCornerShape(
bottomEnd = 18.dp, topStart = 18.dp,
bottomStart = 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( Row(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
horizontalArrangement = if (isOutgoing) Arrangement.End else Arrangement.Start horizontalArrangement = if (isOutgoing) Arrangement.End else Arrangement.Start
) {
Card(
modifier = Modifier.widthIn(max = maxBubbleWidth),
shape = bubbleShape,
colors = CardDefaults.cardColors(containerColor = bubbleColor)
) { ) {
Column( // 使用 Box(fillMaxWidth(0.82f)) 限制气泡最大宽度占比
modifier = Modifier.padding(horizontal = 12.dp, vertical = 8.dp), Box(
verticalArrangement = Arrangement.spacedBy(6.dp) modifier = Modifier.fillMaxWidth(0.82f),
contentAlignment = if (isOutgoing) Alignment.CenterEnd else Alignment.CenterStart
) { ) {
// 接收消息时显示发送者信息 Card(
if (!isOutgoing) { shape = bubbleShape,
Row(verticalAlignment = Alignment.CenterVertically) { 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(
text = message.sender, text = message.content,
style = MaterialTheme.typography.labelLarge, style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.primary 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(
text = message.subtitle, text = formatTime(message.timestampMillis),
style = MaterialTheme.typography.labelSmall, style = MaterialTheme.typography.labelSmall,
color = bubbleTextColor.copy(alpha = 0.75f), color = bubbleTextColor.copy(alpha = 0.7f)
maxLines = 1,
overflow = TextOverflow.Ellipsis
) )
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 modifier 修饰符
* @param state 当前的 UI 状态 * @param state 当前的 UI 状态
* @param onDisplayNameChange 显示名称变更 * @param onDisplayNameChange 显示名称变更
@ -586,6 +560,9 @@ private fun MessageItem(
* @param onConnect 连接服务器 * @param onConnect 连接服务器
* @param onDisconnect 断开连接 * @param onDisconnect 断开连接
* @param onClearMessages 清空消息 * @param onClearMessages 清空消息
* @param onThemeChange 切换主题
* @param onUseDynamicColorChange 切换动态颜色
* @param onLanguageChange 切换语言
*/ */
@Composable @Composable
private fun SettingsTab( private fun SettingsTab(
@ -603,84 +580,61 @@ private fun SettingsTab(
onDisconnect: () -> Unit, onDisconnect: () -> Unit,
onClearMessages: () -> Unit, onClearMessages: () -> Unit,
onThemeChange: (String) -> Unit, onThemeChange: (String) -> Unit,
onUseDynamicColorChange: (Boolean) -> Unit onUseDynamicColorChange: (Boolean) -> Unit,
onLanguageChange: (String) -> Unit
) { ) {
// 定义翻译函数 t
fun t(key: String) = LanguageManager.getString(key, state.language)
LazyColumn( LazyColumn(
modifier = modifier modifier = modifier
.fillMaxSize() .fillMaxSize()
.padding(horizontal = 16.dp, vertical = 8.dp), .padding(horizontal = 16.dp, vertical = 8.dp),
verticalArrangement = Arrangement.spacedBy(12.dp) verticalArrangement = Arrangement.spacedBy(12.dp)
) { ) {
// 个人设置卡片 // 个人设置
item { item {
Card { Card(modifier = Modifier.fillMaxWidth()) {
Column( Column(modifier = Modifier.padding(12.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
modifier = Modifier.padding(12.dp), Text(t("settings.personal"), style = MaterialTheme.typography.titleMedium)
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
Text("个人设置", style = MaterialTheme.typography.titleMedium)
OutlinedTextField( OutlinedTextField(
value = state.displayName, value = state.displayName,
onValueChange = onDisplayNameChange, onValueChange = onDisplayNameChange,
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
label = { Text("显示名称") }, label = { Text(t("settings.display_name")) },
supportingText = { Text("最长 64 字符") },
maxLines = 1 maxLines = 1
) )
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
Button(onClick = onConnect, enabled = state.canConnect) { Button(onClick = onConnect, enabled = state.canConnect) { Text(t("settings.connect")) }
Text("连接") OutlinedButton(onClick = onDisconnect, enabled = state.canDisconnect) { Text(t("settings.disconnect")) }
} OutlinedButton(onClick = onClearMessages) { Text(t("settings.clear_msg")) }
OutlinedButton(onClick = onDisconnect, enabled = state.canDisconnect) {
Text("断开")
}
OutlinedButton(onClick = onClearMessages) {
Text("清空消息")
}
} }
} }
} }
} }
// 服务器管理卡片 // 服务器管理
item { item {
Card { Card(modifier = Modifier.fillMaxWidth()) {
Column( Column(modifier = Modifier.padding(12.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
modifier = Modifier.padding(12.dp), Text(t("settings.server"), style = MaterialTheme.typography.titleMedium)
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
Text("服务器", style = MaterialTheme.typography.titleMedium)
OutlinedTextField( OutlinedTextField(
value = state.serverUrl, value = state.serverUrl,
onValueChange = onServerUrlChange, onValueChange = onServerUrlChange,
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
label = { Text("服务器地址") }, label = { Text(t("settings.server_url")) },
placeholder = { Text("ws://10.0.2.2:13173/") },
maxLines = 1 maxLines = 1
) )
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
Button(onClick = onSaveServer) { Button(onClick = onSaveServer) { Text(t("settings.save_server")) }
Text("保存地址") OutlinedButton(onClick = onRemoveServer) { Text(t("settings.remove_current")) }
}
OutlinedButton(onClick = onRemoveServer) {
Text("删除当前")
}
} }
if (state.serverUrls.isNotEmpty()) { if (state.serverUrls.isNotEmpty()) {
HorizontalDivider() HorizontalDivider()
Text("已保存地址", style = MaterialTheme.typography.labelLarge) Text(t("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(onClick = { onSelectServer(url) }, label = { Text(url, maxLines = 1, overflow = TextOverflow.Ellipsis) })
onClick = { onSelectServer(url) },
label = {
Text(
text = url,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
)
} }
} }
} }
@ -688,80 +642,66 @@ private fun SettingsTab(
} }
} }
// 身份与安全卡片 // 身份与安全
item { item {
Card { Card(modifier = Modifier.fillMaxWidth()) {
Column( Column(modifier = Modifier.padding(12.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
modifier = Modifier.padding(12.dp), Text(t("settings.identity"), style = MaterialTheme.typography.titleMedium)
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
Text("身份与安全", style = MaterialTheme.typography.titleMedium)
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
Button( Button(onClick = onRevealPublicKey, enabled = !state.loadingPublicKey) { Text(if (state.loadingPublicKey) "..." else t("settings.reveal_key")) }
onClick = onRevealPublicKey, OutlinedButton(onClick = onCopyPublicKey, enabled = state.myPublicKey.isNotBlank()) { Text(t("settings.copy_key")) }
enabled = !state.loadingPublicKey
) {
Text(if (state.loadingPublicKey) "读取中" else "查看/生成公钥")
}
OutlinedButton(
onClick = onCopyPublicKey,
enabled = state.myPublicKey.isNotBlank()
) {
Text("复制公钥")
}
} }
OutlinedTextField( OutlinedTextField(value = state.myPublicKey, onValueChange = {}, modifier = Modifier.fillMaxWidth(), readOnly = true, label = { Text(t("settings.my_key")) }, maxLines = 4)
value = state.myPublicKey,
onValueChange = {},
modifier = Modifier.fillMaxWidth(),
readOnly = true,
label = { Text("我的公钥") },
placeholder = { Text("点击“查看/生成公钥”") },
maxLines = 4
)
} }
} }
} }
// 主题设置卡片 // 语言选择
item { item {
Card { Card(modifier = Modifier.fillMaxWidth()) {
Column( Column(modifier = Modifier.padding(12.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
modifier = Modifier.padding(12.dp), Text(t("settings.language"), style = MaterialTheme.typography.titleMedium)
verticalArrangement = Arrangement.spacedBy(8.dp) LazyRow(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
) { items(LanguageManager.supportedLanguages) { lang ->
Text("主题", style = MaterialTheme.typography.titleMedium) FilterChip(
selected = state.language == lang.code,
// 动态颜色开关(仅 Android 12+ 显示) onClick = { onLanguageChange(lang.code) },
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { label = { Text(lang.name) },
Row( leadingIcon = { Icon(Icons.Rounded.Language, contentDescription = null, modifier = Modifier.size(16.dp)) }
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
Switch(
checked = state.useDynamicColor,
onCheckedChange = onUseDynamicColorChange
) )
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) { 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)) { LazyRow(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
items(themeOptions) { option -> 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( FilterChip(
selected = state.themeId == option.id, selected = state.themeId == option.id,
onClick = { onThemeChange(option.id) }, onClick = { onThemeChange(option.id) },
label = { Text(option.name) }, label = { Text(themeName) },
leadingIcon = { leadingIcon = { Box(modifier = Modifier.size(16.dp).background(option.primary, RoundedCornerShape(4.dp))) }
Box(
modifier = Modifier
.size(16.dp)
.background(option.primary, RoundedCornerShape(4.dp))
)
}
) )
} }
} }
@ -770,34 +710,23 @@ private fun SettingsTab(
} }
} }
// 诊断信息卡片 // 诊断信息
item { item {
Card { Card(modifier = Modifier.fillMaxWidth()) {
Column( Column(modifier = Modifier.padding(12.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
modifier = Modifier.padding(12.dp), Text(t("settings.diagnostics"), style = MaterialTheme.typography.titleMedium)
verticalArrangement = Arrangement.spacedBy(8.dp) Text("${t("settings.status_hint")}${state.statusHint}")
) { Text("${t("settings.current_status")}${state.statusText}")
Text("诊断", style = MaterialTheme.typography.titleMedium) Text("${t("settings.cert_fingerprint")}${state.certFingerprint.ifBlank { "N/A" }}")
Text("连接提示:${state.statusHint}") Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp)) {
Text("当前状态:${state.statusText}") Switch(checked = state.showSystemMessages, onCheckedChange = onToggleShowSystem)
Text("证书指纹:${state.certFingerprint.ifBlank { "未获取" }}") Text(t("settings.show_system"))
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
Switch(
checked = state.showSystemMessages,
onCheckedChange = onToggleShowSystem
)
Text("显示系统消息")
} }
} }
} }
} }
item { item { Spacer(modifier = Modifier.height(12.dp)) }
Spacer(modifier = Modifier.height(12.dp))
}
} }
} }

@ -183,8 +183,9 @@ object ChatSessionManager {
serverUrl = pref.currentServerUrl, serverUrl = pref.currentServerUrl,
directMode = pref.directMode, directMode = pref.directMode,
showSystemMessages = pref.showSystemMessages, showSystemMessages = pref.showSystemMessages,
themeId = pref.themeId, // 假设 preferences 中有 themeId themeId = pref.themeId,
useDynamicColor = pref.useDynamicColor 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 主题名 * @param enabled 主题名
@ -519,7 +531,7 @@ object ChatSessionManager {
} }
/** /**
* 确保本地身份已加载创建 * 确保本地身份已加载 or 创建
* @return 本地身份对象 * @return 本地身份对象
*/ */
private suspend fun ensureIdentity(): RsaCryptoManager.Identity { private suspend fun ensureIdentity(): RsaCryptoManager.Identity {
@ -533,7 +545,7 @@ object ChatSessionManager {
} }
/** /**
* 处理收到的原始文本消息可能是握手包加密消息 * 处理收到的原始文本消息可能是握手包 or 加密消息
* @param rawText 原始文本 * @param rawText 原始文本
*/ */
private suspend fun handleIncomingMessage(rawText: String) { private suspend fun handleIncomingMessage(rawText: String) {

@ -67,6 +67,7 @@ data class UiMessage(
* @property myPublicKey 本地公钥 * @property myPublicKey 本地公钥
* @property sending 是否正在发送消息用于禁用按钮 * @property sending 是否正在发送消息用于禁用按钮
* @property loadingPublicKey 是否正在加载公钥 * @property loadingPublicKey 是否正在加载公钥
* @property language 当前选择的语言代码 ( "zh", "en", "ja")
*/ */
data class ChatUiState( data class ChatUiState(
val status: ConnectionStatus = ConnectionStatus.IDLE, val status: ConnectionStatus = ConnectionStatus.IDLE,
@ -83,8 +84,9 @@ data class ChatUiState(
val myPublicKey: String = "", val myPublicKey: String = "",
val sending: Boolean = false, val sending: Boolean = false,
val loadingPublicKey: Boolean = false, val loadingPublicKey: Boolean = false,
val themeId: String = "default", /* 当前选中的主题名 (@emilia-t)*/ val themeId: String = "blue",
val useDynamicColor: Boolean = true /* 是否使用 Android 12+ 动态颜色 (@emilia-t)*/ val useDynamicColor: Boolean = true,
val language: String = "zh"
) { ) {
/** /**
* 是否允许连接 * 是否允许连接

@ -5,7 +5,7 @@ import androidx.lifecycle.AndroidViewModel
/** /**
* ViewModel 作为 UI [ChatSessionManager] 的桥梁 * ViewModel 作为 UI [ChatSessionManager] 的桥梁
* 初始化会话管理器并暴露其状态事件流同时提供所有用户操作的代理方法 * 初始化会话管理器并暴露其状态 and 事件流同时提供所有用户操作的代理方法
* @param application Application 实例 * @param application Application 实例
*/ */
class ChatViewModel(application: Application) : AndroidViewModel(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 updateTheme(themeId: String) = ChatSessionManager.updateTheme(themeId)
fun updateUseDynamicColor(enabled: Boolean) = ChatSessionManager.updateUseDynamicColor(enabled) fun updateUseDynamicColor(enabled: Boolean) = ChatSessionManager.updateUseDynamicColor(enabled)
fun updateLanguage(language: String) = ChatSessionManager.updateLanguage(language)
} }

@ -2,10 +2,16 @@ package com.onlinemsg.client.ui.theme
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
// 应用的主色调、表面色、错误色等定义 // 浅色模式 - 温暖色调定义
val Primary = Color(0xFF0C6D62) val Primary = Color(0xFFD84315) // 深橙色 800
val OnPrimary = Color(0xFFFFFFFF) val OnPrimary = Color(0xFFFFFFFF)
val Secondary = Color(0xFF4A635F) val Secondary = Color(0xFF6D4C41) // 棕色 600
val Surface = Color(0xFFF7FAF8) val Surface = Color(0xFFFFFBF0) // 温暖的奶油白
val SurfaceVariant = Color(0xFFDCE8E4) val SurfaceVariant = Color(0xFFFBE9E7) // 极浅橙色
val Error = Color(0xFFB3261E) val Error = Color(0xFFB00020)
// 深色模式 - 温暖色调定义
val DarkPrimary = Color(0xFFFFB5A0) // 浅暖橙
val DarkSecondary = Color(0xFFD7C1B1) // 浅棕
val DarkSurface = Color(0xFF1D1B16) // 暖深褐色(解决深色模式下背景过亮问题)
val OnDarkSurface = Color(0xFFEDE0DD)

@ -21,8 +21,11 @@ private val LightColors = lightColorScheme(
) )
private val DarkColors = darkColorScheme( private val DarkColors = darkColorScheme(
primary = Primary.copy(alpha = 0.9f), primary = DarkPrimary,
secondary = Secondary.copy(alpha = 0.9f), onPrimary = Color.Black,
secondary = DarkSecondary,
surface = DarkSurface,
onSurface = OnDarkSurface,
error = Error error = Error
) )
@ -30,40 +33,39 @@ private val DarkColors = darkColorScheme(
* 应用主题可组合函数 * 应用主题可组合函数
* 支持浅色/深色模式以及 Android 12+ 的动态颜色 * 支持浅色/深色模式以及 Android 12+ 的动态颜色
* @param darkTheme 是否强制深色模式默认跟随系统 * @param darkTheme 是否强制深色模式默认跟随系统
* @param dynamicColor 是否启用动态颜色默认 true * @param themeId 当前选中的主题 ID (默认为 "blue")
* @param content 内部内容 * @param useDynamicColor 是否启用动态颜色Android 12+ 支持
*/ */
@Composable @Composable
fun OnlineMsgTheme( fun OnlineMsgTheme(
darkTheme: Boolean = isSystemInDarkTheme(), darkTheme: Boolean = isSystemInDarkTheme(),
themeId: String = "default", // 从外部传入 themeId: String = "blue", // 默认预设设为 blue
useDynamicColor: Boolean = true, // 从外部传入 useDynamicColor: Boolean = true,
content: @Composable () -> Unit content: @Composable () -> Unit
) { ) {
val context = LocalContext.current val context = LocalContext.current
val colorScheme = when { val colorScheme = when {
// 优先使用动态颜色(如果启用且系统支持) // 1. 优先使用动态颜色(如果启用且系统支持)
useDynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { useDynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
} }
// 否则根据主题 ID 选择预设 // 2. 根据 themeId 选择预设
else -> { else -> {
val option = themeOptions.find { it.id == themeId } ?: themeOptions.first() val option = themeOptions.find { it.id == themeId } ?: themeOptions.first()
if (darkTheme) { if (darkTheme) {
darkColorScheme( darkColorScheme(
primary = option.primary, primary = option.primaryDark ?: option.primary.copy(alpha = 0.8f),
secondary = option.secondary, secondary = option.secondary.copy(alpha = 0.8f),
error = option.error ?: Error, surface = option.surfaceDark ?: DarkSurface,
surface = option.surface ?: Surface, onSurface = OnDarkSurface,
// 其他颜色可以沿用默认深色调整 error = option.error ?: Error
) )
} else { } else {
lightColorScheme( lightColorScheme(
primary = option.primary, primary = option.primary,
secondary = option.secondary, secondary = option.secondary,
error = option.error ?: Error,
surface = option.surface ?: Surface, surface = option.surface ?: Surface,
// 其他颜色保持默认浅色 error = option.error ?: Error
) )
} }
} }
@ -83,13 +85,38 @@ data class ThemeOption(
val primary: Color, val primary: Color,
val secondary: Color, val secondary: Color,
val surface: Color? = null, val surface: Color? = null,
val primaryDark: Color? = null, // 显式深色主色
val surfaceDark: Color? = null, // 显式深色背景
val error: Color? = null val error: Color? = null
) )
// 预设主题列表 // 预设主题列表
val themeOptions = listOf( val themeOptions = listOf(
ThemeOption("default", "默认", Primary, Secondary), // 默认列表首位即为默认主题
ThemeOption("blue", "蔚蓝", Color(0xFF1E88E5), Color(0xFF6A8DAA)), ThemeOption(
ThemeOption("green", "翠绿", Color(0xFF2E7D32), Color(0xFF4A635F)), id = "blue",
ThemeOption("red", "绯红", Color(0xFFC62828), Color(0xFF8D6E63)) 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)
)
) )

@ -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)
Loading…
Cancel
Save