feat: 更新Android UI界面
- 更新创建记忆界面,改进用户体验 - 更新个人资料界面,添加更多功能 - 更新应用图标资源 - 更新CreateMemoryViewModel以支持新功能 - 更新ViewModelFactory
This commit is contained in:
@@ -1,6 +1,11 @@
|
||||
package com.huaga.life_echo.ui.icons
|
||||
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.automirrored.filled.Chat
|
||||
import androidx.compose.material.icons.automirrored.filled.Help
|
||||
import androidx.compose.material.icons.automirrored.filled.MenuBook
|
||||
import androidx.compose.material.icons.automirrored.filled.Send
|
||||
import androidx.compose.material.icons.filled.*
|
||||
import androidx.compose.material.icons.outlined.*
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
@@ -11,18 +16,18 @@ import androidx.compose.ui.graphics.vector.ImageVector
|
||||
*/
|
||||
object AppIcons {
|
||||
// 导航栏图标
|
||||
val Chat = Icons.Default.Chat
|
||||
val Memoir = Icons.Default.MenuBook
|
||||
val Chat = Icons.AutoMirrored.Filled.Chat
|
||||
val Memoir = Icons.AutoMirrored.Filled.MenuBook
|
||||
val Profile = Icons.Default.Person
|
||||
|
||||
// 聊天界面图标
|
||||
val ArrowBack = Icons.Default.ArrowBack
|
||||
val ArrowBack = Icons.AutoMirrored.Filled.ArrowBack
|
||||
val Mic = Icons.Default.Mic
|
||||
val MicOff = Icons.Default.MicOff
|
||||
val SentimentSatisfied = Icons.Default.SentimentSatisfied
|
||||
val Add = Icons.Default.Add
|
||||
val Send = Icons.Default.Send
|
||||
val Book = Icons.Default.MenuBook
|
||||
val Send = Icons.AutoMirrored.Filled.Send
|
||||
val Book = Icons.AutoMirrored.Filled.MenuBook
|
||||
|
||||
// 对话列表图标
|
||||
val Conversation = Icons.Default.ChatBubble
|
||||
@@ -31,7 +36,7 @@ object AppIcons {
|
||||
|
||||
// 回忆录界面图标
|
||||
val Chapter = Icons.Default.Book
|
||||
val Reading = Icons.Default.MenuBook
|
||||
val Reading = Icons.AutoMirrored.Filled.MenuBook
|
||||
val ChevronRight = Icons.Default.ChevronRight
|
||||
val Edit = Icons.Default.Edit
|
||||
val Share = Icons.Default.Share
|
||||
@@ -46,7 +51,7 @@ object AppIcons {
|
||||
val FormatSize = Icons.Default.FormatSize
|
||||
val Brightness2 = Icons.Default.Brightness2
|
||||
val Settings = Icons.Default.Settings
|
||||
val Help = Icons.Default.Help
|
||||
val Help = Icons.AutoMirrored.Filled.Help
|
||||
val Info = Icons.Default.Info
|
||||
|
||||
// 其他常用图标
|
||||
@@ -69,4 +74,8 @@ object AppIcons {
|
||||
val Error = Icons.Default.Error
|
||||
val Warning = Icons.Default.Warning
|
||||
val InfoCircle = Icons.Default.Info
|
||||
|
||||
// 认证相关图标
|
||||
val Visibility = Icons.Default.Visibility
|
||||
val VisibilityOff = Icons.Default.VisibilityOff
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.huaga.life_echo.ui.screens
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.*
|
||||
@@ -12,6 +13,7 @@ import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.draw.shadow
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
@@ -35,6 +37,12 @@ fun CreateMemoryScreen(
|
||||
val transcript by viewModel.transcript.collectAsState()
|
||||
val agentResponse by viewModel.agentResponse.collectAsState()
|
||||
val connectionStatus by viewModel.connectionStatus.collectAsState()
|
||||
val userMessages by viewModel.userMessages.collectAsState()
|
||||
|
||||
// 输入框状态
|
||||
var showTextInput by remember { mutableStateOf(false) }
|
||||
var inputText by remember { mutableStateOf("") }
|
||||
val keyboardController = LocalSoftwareKeyboardController.current
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
@@ -142,6 +150,31 @@ fun CreateMemoryScreen(
|
||||
}
|
||||
}
|
||||
|
||||
// 用户文本消息
|
||||
userMessages.forEach { message ->
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||
horizontalArrangement = Arrangement.End
|
||||
) {
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.shadow(2.dp, RoundedCornerShape(12.dp)),
|
||||
shape = RoundedCornerShape(12.dp),
|
||||
colors = CardDefaults.cardColors(containerColor = LightPurple.copy(alpha = 0.2f))
|
||||
) {
|
||||
Text(
|
||||
text = message,
|
||||
modifier = Modifier.padding(12.dp),
|
||||
fontSize = 14.sp,
|
||||
color = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 用户消息(转写文本)
|
||||
if (transcript.isNotEmpty()) {
|
||||
Row(
|
||||
@@ -236,21 +269,92 @@ fun CreateMemoryScreen(
|
||||
)
|
||||
}
|
||||
|
||||
// 语音输入提示区域(只读,不支持文本输入)
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.height(40.dp)
|
||||
.clip(RoundedCornerShape(20.dp))
|
||||
.background(MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.3f)),
|
||||
contentAlignment = Alignment.CenterStart
|
||||
) {
|
||||
Text(
|
||||
text = if (isRecording) "正在录音..." else "说点什么...",
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
fontSize = 14.sp
|
||||
)
|
||||
// 输入框区域 - 点击弹出文本输入
|
||||
if (showTextInput) {
|
||||
// 显示可编辑的文本输入框
|
||||
Row(
|
||||
modifier = Modifier.weight(1f),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
OutlinedTextField(
|
||||
value = inputText,
|
||||
onValueChange = { inputText = it },
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.height(40.dp),
|
||||
placeholder = {
|
||||
Text(
|
||||
text = "输入消息...",
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
fontSize = 14.sp
|
||||
)
|
||||
},
|
||||
shape = RoundedCornerShape(20.dp),
|
||||
colors = OutlinedTextFieldDefaults.colors(
|
||||
unfocusedBorderColor = Color.Transparent,
|
||||
focusedBorderColor = LightPurple,
|
||||
focusedContainerColor = MaterialTheme.colorScheme.surface,
|
||||
unfocusedContainerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.3f)
|
||||
),
|
||||
singleLine = true,
|
||||
maxLines = 1
|
||||
)
|
||||
|
||||
// 发送按钮
|
||||
if (inputText.isNotBlank()) {
|
||||
IconButton(
|
||||
onClick = {
|
||||
viewModel.sendTextMessage(inputText)
|
||||
inputText = ""
|
||||
showTextInput = false
|
||||
keyboardController?.hide()
|
||||
}
|
||||
) {
|
||||
Icon(
|
||||
imageVector = AppIcons.Send,
|
||||
contentDescription = "发送",
|
||||
tint = LightPurple,
|
||||
modifier = Modifier.size(24.dp)
|
||||
)
|
||||
}
|
||||
} else {
|
||||
// 关闭按钮(当输入框为空时)
|
||||
IconButton(
|
||||
onClick = {
|
||||
showTextInput = false
|
||||
keyboardController?.hide()
|
||||
}
|
||||
) {
|
||||
Icon(
|
||||
imageVector = AppIcons.Close,
|
||||
contentDescription = "关闭",
|
||||
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.size(24.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 显示只读提示区域
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.height(40.dp)
|
||||
.clip(RoundedCornerShape(20.dp))
|
||||
.background(MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.3f))
|
||||
.clickable {
|
||||
showTextInput = true
|
||||
keyboardController?.show()
|
||||
},
|
||||
contentAlignment = Alignment.CenterStart
|
||||
) {
|
||||
Text(
|
||||
text = if (isRecording) "正在录音..." else "说点什么...",
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
fontSize = 14.sp
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// 表情图标
|
||||
|
||||
@@ -35,7 +35,10 @@ import androidx.compose.runtime.setValue
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.RadioButton
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
@@ -43,18 +46,37 @@ import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.huaga.life_echo.data.auth.TokenManager
|
||||
import com.huaga.life_echo.navigation.Screen
|
||||
import com.huaga.life_echo.ui.icons.AppIcons
|
||||
import com.huaga.life_echo.ui.settings.AppSettings
|
||||
import com.huaga.life_echo.ui.theme.LightPurple
|
||||
import com.huaga.life_echo.ui.viewmodel.AuthViewModel
|
||||
import com.huaga.life_echo.ui.viewmodel.ViewModelFactory
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
|
||||
@Composable
|
||||
fun ProfileScreen(
|
||||
navController: androidx.navigation.NavHostController? = null
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val authViewModel: AuthViewModel = viewModel(factory = ViewModelFactory(context))
|
||||
|
||||
var largeFontMode by remember { mutableStateOf(AppSettings.largeFontMode) }
|
||||
var darkMode by remember { mutableStateOf(AppSettings.darkMode) }
|
||||
var speechRate by remember { mutableStateOf(AppSettings.speechRate) }
|
||||
var showSpeechRateDialog by remember { mutableStateOf(false) }
|
||||
var showLogoutDialog by remember { mutableStateOf(false) }
|
||||
|
||||
val isLoggedIn by authViewModel.isLoggedIn.collectAsState()
|
||||
val currentUser by authViewModel.currentUser.collectAsState()
|
||||
|
||||
// 初始化TokenManager
|
||||
LaunchedEffect(Unit) {
|
||||
TokenManager.initialize(context)
|
||||
authViewModel.checkAuthStatus()
|
||||
}
|
||||
|
||||
// 应用设置变化 - 立即更新全局设置
|
||||
LaunchedEffect(largeFontMode) {
|
||||
@@ -121,48 +143,123 @@ fun ProfileScreen(
|
||||
.padding(top = 16.dp, bottom = 32.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
// 用户头像
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(80.dp)
|
||||
.clip(CircleShape)
|
||||
.background(LightPurple.copy(alpha = 0.2f)),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Icon(
|
||||
imageVector = AppIcons.Person,
|
||||
contentDescription = "用户头像",
|
||||
tint = LightPurple,
|
||||
modifier = Modifier.size(48.dp)
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
Text(
|
||||
text = "李明华",
|
||||
fontSize = 20.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
if (isLoggedIn && currentUser != null) {
|
||||
// 已登录:显示用户信息
|
||||
// 用户头像
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(8.dp)
|
||||
.size(80.dp)
|
||||
.clip(CircleShape)
|
||||
.background(LightPurple)
|
||||
)
|
||||
Spacer(modifier = Modifier.width(4.dp))
|
||||
.background(LightPurple.copy(alpha = 0.2f)),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Icon(
|
||||
imageVector = AppIcons.Person,
|
||||
contentDescription = "用户头像",
|
||||
tint = LightPurple,
|
||||
modifier = Modifier.size(48.dp)
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
Text(
|
||||
text = "免费体验版",
|
||||
fontSize = 12.sp,
|
||||
text = currentUser!!.nickname,
|
||||
fontSize = 20.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(8.dp)
|
||||
.clip(CircleShape)
|
||||
.background(LightPurple)
|
||||
)
|
||||
Spacer(modifier = Modifier.width(4.dp))
|
||||
Text(
|
||||
text = when (currentUser!!.subscription_type) {
|
||||
"free" -> "免费体验版"
|
||||
"premium" -> "高级版"
|
||||
"professional" -> "专业版"
|
||||
else -> "免费体验版"
|
||||
},
|
||||
fontSize = 12.sp,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
// 登出按钮
|
||||
Button(
|
||||
onClick = { showLogoutDialog = true },
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = MaterialTheme.colorScheme.error
|
||||
),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 32.dp)
|
||||
.height(40.dp)
|
||||
) {
|
||||
Text("登出", fontSize = 14.sp)
|
||||
}
|
||||
} else {
|
||||
// 未登录:显示登录提示
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(80.dp)
|
||||
.clip(CircleShape)
|
||||
.background(LightPurple.copy(alpha = 0.2f)),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Icon(
|
||||
imageVector = AppIcons.Person,
|
||||
contentDescription = "用户头像",
|
||||
tint = LightPurple,
|
||||
modifier = Modifier.size(48.dp)
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
Text(
|
||||
text = "未登录",
|
||||
fontSize = 20.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
Text(
|
||||
text = "登录以同步您的数据",
|
||||
fontSize = 14.sp,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
|
||||
// 登录按钮
|
||||
Button(
|
||||
onClick = {
|
||||
navController?.navigate(Screen.Login.route)
|
||||
},
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = LightPurple
|
||||
),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 32.dp)
|
||||
.height(48.dp)
|
||||
) {
|
||||
Text("登录", fontSize = 16.sp, fontWeight = FontWeight.Bold)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -263,6 +360,30 @@ fun ProfileScreen(
|
||||
Spacer(modifier = Modifier.height(32.dp))
|
||||
}
|
||||
}
|
||||
|
||||
// 登出确认对话框
|
||||
if (showLogoutDialog) {
|
||||
AlertDialog(
|
||||
onDismissRequest = { showLogoutDialog = false },
|
||||
title = { Text("确认登出") },
|
||||
text = { Text("确定要登出吗?") },
|
||||
confirmButton = {
|
||||
TextButton(
|
||||
onClick = {
|
||||
authViewModel.logout()
|
||||
showLogoutDialog = false
|
||||
}
|
||||
) {
|
||||
Text("确认", color = MaterialTheme.colorScheme.error)
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = { showLogoutDialog = false }) {
|
||||
Text("取消")
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
package com.huaga.life_echo.ui.viewmodel
|
||||
|
||||
import android.content.Context
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.huaga.life_echo.data.auth.TokenManager
|
||||
import com.huaga.life_echo.data.repository.ConversationRepository
|
||||
import com.huaga.life_echo.data.repository.ChapterRepository
|
||||
import com.huaga.life_echo.network.WebSocketClient
|
||||
@@ -13,7 +15,8 @@ import kotlinx.coroutines.launch
|
||||
|
||||
class CreateMemoryViewModel(
|
||||
private val conversationRepository: ConversationRepository,
|
||||
private val chapterRepository: ChapterRepository
|
||||
private val chapterRepository: ChapterRepository,
|
||||
private val context: Context
|
||||
) : ViewModel() {
|
||||
|
||||
private val webSocketClient = WebSocketClient()
|
||||
@@ -23,6 +26,11 @@ class CreateMemoryViewModel(
|
||||
val agentResponse = MutableStateFlow("")
|
||||
val connectionStatus = MutableStateFlow("未连接")
|
||||
val conversationId = MutableStateFlow<String?>(null)
|
||||
val userMessages = MutableStateFlow<List<String>>(emptyList()) // 用户发送的文本消息列表
|
||||
|
||||
init {
|
||||
TokenManager.initialize(context)
|
||||
}
|
||||
|
||||
fun startConversation() {
|
||||
viewModelScope.launch {
|
||||
@@ -32,7 +40,9 @@ class CreateMemoryViewModel(
|
||||
connectionStatus.value = "连接中..."
|
||||
|
||||
try {
|
||||
webSocketClient.connect(convId) { message ->
|
||||
// 获取访问令牌
|
||||
val token = TokenManager.getAccessToken()
|
||||
webSocketClient.connect(convId, token) { message ->
|
||||
handleWebSocketMessage(message)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
@@ -61,6 +71,29 @@ class CreateMemoryViewModel(
|
||||
}
|
||||
}
|
||||
|
||||
fun sendTextMessage(text: String) {
|
||||
if (text.isBlank()) return
|
||||
|
||||
viewModelScope.launch {
|
||||
// 确保已连接
|
||||
if (conversationId.value == null) {
|
||||
startConversation()
|
||||
}
|
||||
|
||||
conversationId.value?.let { id ->
|
||||
// 添加到用户消息列表
|
||||
userMessages.value = userMessages.value + text
|
||||
|
||||
// 发送文本消息
|
||||
try {
|
||||
webSocketClient.sendTextMessage(text, id)
|
||||
} catch (e: Exception) {
|
||||
connectionStatus.value = "发送失败: ${e.message}"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleWebSocketMessage(message: WebSocketMessage) {
|
||||
when (message.type) {
|
||||
MessageType.transcript -> {
|
||||
@@ -69,6 +102,9 @@ class CreateMemoryViewModel(
|
||||
MessageType.agent_response -> {
|
||||
agentResponse.value = message.data["text"] ?: ""
|
||||
}
|
||||
MessageType.text -> {
|
||||
// 处理文本消息响应(如果需要)
|
||||
}
|
||||
MessageType.connect -> {
|
||||
connectionStatus.value = "已连接"
|
||||
}
|
||||
|
||||
@@ -30,7 +30,8 @@ class ViewModelFactory(private val context: Context) : ViewModelProvider.Factory
|
||||
modelClass.isAssignableFrom(CreateMemoryViewModel::class.java) -> {
|
||||
CreateMemoryViewModel(
|
||||
conversationRepository = conversationRepository,
|
||||
chapterRepository = chapterRepository
|
||||
chapterRepository = chapterRepository,
|
||||
context = context
|
||||
) as T
|
||||
}
|
||||
modelClass.isAssignableFrom(ConversationListViewModel::class.java) -> {
|
||||
@@ -44,6 +45,9 @@ class ViewModelFactory(private val context: Context) : ViewModelProvider.Factory
|
||||
apiService = apiService
|
||||
) as T
|
||||
}
|
||||
modelClass.isAssignableFrom(AuthViewModel::class.java) -> {
|
||||
AuthViewModel(context = context) as T
|
||||
}
|
||||
else -> throw IllegalArgumentException("Unknown ViewModel class: ${modelClass.name}")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user