feat: 新增功能组件和应用配置
- 新增OrganizeConversationDialog组织对话对话框 - 新增debug调试组件 - 新增network_security_config网络安全配置 - 更新MainActivity主活动 - 更新AppConfig应用配置 - 更新ConversationRepository数据仓库 - 更新build.gradle和AndroidManifest配置
This commit is contained in:
@@ -63,7 +63,8 @@ dependencies {
|
||||
|
||||
// Ktor Client
|
||||
implementation(libs.ktor.client.core)
|
||||
implementation(libs.ktor.client.android)
|
||||
implementation(libs.ktor.client.android) // 用于普通HTTP请求
|
||||
implementation(libs.ktor.client.okhttp) // 用于WebSocket(OkHttp引擎支持WebSocket)
|
||||
implementation(libs.ktor.client.websockets)
|
||||
implementation(libs.ktor.client.content.negotiation)
|
||||
implementation(libs.ktor.serialization.kotlinx.json)
|
||||
@@ -81,6 +82,9 @@ dependencies {
|
||||
// Serialization
|
||||
implementation(libs.kotlinx.serialization.json)
|
||||
|
||||
// Image Loading
|
||||
implementation(libs.coil.compose)
|
||||
|
||||
// Permissions
|
||||
implementation(libs.accompanist.permissions)
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
android:fullBackupContent="@xml/backup_rules"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:networkSecurityConfig="@xml/network_security_config"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/Theme.Lifeecho" >
|
||||
|
||||
@@ -46,8 +46,11 @@ import androidx.compose.ui.tooling.preview.PreviewScreenSizes
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.core.view.WindowCompat
|
||||
import androidx.core.view.WindowInsetsControllerCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import androidx.navigation.compose.currentBackStackEntryAsState
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import com.huaga.life_echo.data.auth.TokenManager
|
||||
import com.huaga.life_echo.navigation.AppNavigation
|
||||
import com.huaga.life_echo.navigation.Screen
|
||||
@@ -81,12 +84,61 @@ class MainActivity : ComponentActivity() {
|
||||
windowInsetsController.isAppearanceLightStatusBars = !darkMode
|
||||
windowInsetsController.isAppearanceLightNavigationBars = !darkMode
|
||||
}
|
||||
// 隐藏系统状态栏和导航栏
|
||||
SystemUiController(
|
||||
isStatusBarVisible = false,
|
||||
isNavigationBarVisible = false
|
||||
)
|
||||
LifeechoApp(TokenManager.isLoggedIn)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 系统UI控制器 Composable 函数
|
||||
* 用于控制状态栏和导航栏的显示/隐藏
|
||||
*
|
||||
* @param isStatusBarVisible 状态栏是否可见,false 表示隐藏
|
||||
* @param isNavigationBarVisible 导航栏是否可见,false 表示隐藏
|
||||
*/
|
||||
@Composable
|
||||
fun SystemUiController(
|
||||
isStatusBarVisible: Boolean = true,
|
||||
isNavigationBarVisible: Boolean = true
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val activity = context as? ComponentActivity
|
||||
|
||||
androidx.compose.runtime.LaunchedEffect(isStatusBarVisible, isNavigationBarVisible) {
|
||||
activity?.window?.let { window ->
|
||||
// 让内容延伸到系统栏下方
|
||||
WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||
|
||||
// 获取 WindowInsetsController
|
||||
val windowInsetsController = WindowCompat.getInsetsController(window, window.decorView)
|
||||
|
||||
// 控制状态栏的显示/隐藏
|
||||
if (isStatusBarVisible) {
|
||||
windowInsetsController.show(WindowInsetsCompat.Type.statusBars())
|
||||
} else {
|
||||
windowInsetsController.hide(WindowInsetsCompat.Type.statusBars())
|
||||
}
|
||||
|
||||
// 控制导航栏的显示/隐藏
|
||||
if (isNavigationBarVisible) {
|
||||
windowInsetsController.show(WindowInsetsCompat.Type.navigationBars())
|
||||
} else {
|
||||
windowInsetsController.hide(WindowInsetsCompat.Type.navigationBars())
|
||||
}
|
||||
|
||||
// 设置系统栏行为:当隐藏时,内容可以延伸到系统栏区域
|
||||
windowInsetsController.systemBarsBehavior =
|
||||
WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
|
||||
@PreviewScreenSizes
|
||||
@Composable
|
||||
|
||||
@@ -1,12 +1,23 @@
|
||||
package com.huaga.life_echo.config
|
||||
|
||||
object AppConfig {
|
||||
// API 基础 URL - 内网地址,用于同一局域网下的物理机测试
|
||||
// 当前主机IP: 192.168.10.9,端口: 8000
|
||||
// 如果IP地址变化,请修改此处
|
||||
// API 基础 URL 配置
|
||||
//
|
||||
// 物理机测试:使用实际的内网IP地址(如 192.168.10.9)
|
||||
// Android模拟器:使用 10.0.2.2 来访问主机(开发机器)
|
||||
//
|
||||
// 注意:从Android 9 (API 28)开始,默认禁止明文HTTP流量
|
||||
// 已在 network_security_config.xml 中配置允许明文流量(仅用于开发环境)
|
||||
// 生产环境应该使用HTTPS并移除明文流量配置
|
||||
|
||||
// 当前配置:物理机测试
|
||||
const val BASE_URL = "http://192.168.10.9:8000"
|
||||
const val WS_BASE_URL = "ws://192.168.10.9:8000"
|
||||
|
||||
// 如果需要在Android模拟器上测试,请使用以下配置:
|
||||
// const val BASE_URL = "http://10.0.2.2:8000"
|
||||
// const val WS_BASE_URL = "ws://10.0.2.2:8000"
|
||||
|
||||
// 生产环境应该从配置文件或环境变量读取
|
||||
// const val BASE_URL = BuildConfig.API_BASE_URL
|
||||
}
|
||||
@@ -26,6 +26,22 @@ class ConversationRepository(
|
||||
conversationDao.updateConversation(conversation)
|
||||
}
|
||||
|
||||
suspend fun deleteConversation(id: String): Result<Unit> {
|
||||
val result = apiService.deleteConversation(id)
|
||||
return result.fold(
|
||||
onSuccess = {
|
||||
// 从本地数据库删除
|
||||
conversationDao.getConversationById(id)?.let { conversation ->
|
||||
conversationDao.deleteConversation(conversation)
|
||||
}
|
||||
Result.success(Unit)
|
||||
},
|
||||
onFailure = { exception ->
|
||||
Result.failure(exception)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
fun getSegmentsByConversationId(conversationId: String): Flow<List<ConversationSegment>> {
|
||||
return segmentDao.getSegmentsByConversationId(conversationId)
|
||||
}
|
||||
@@ -38,31 +54,72 @@ class ConversationRepository(
|
||||
segmentDao.insertSegments(segments)
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建新对话
|
||||
*/
|
||||
suspend fun createConversation(): Result<String> {
|
||||
val result = apiService.createConversation()
|
||||
return result.fold(
|
||||
onSuccess = { response ->
|
||||
// 将新对话保存到本地数据库
|
||||
val conversation = Conversation(
|
||||
id = response.id,
|
||||
userId = response.user_id,
|
||||
startedAt = try {
|
||||
java.time.Instant.parse(response.started_at).toEpochMilli()
|
||||
} catch (e: Exception) {
|
||||
System.currentTimeMillis()
|
||||
},
|
||||
endedAt = null,
|
||||
durationSeconds = 0,
|
||||
summary = null,
|
||||
currentTopic = null,
|
||||
conversationStage = null,
|
||||
avatarUrl = null,
|
||||
title = null,
|
||||
latestMessagePreview = null,
|
||||
latestMessageTime = System.currentTimeMillis()
|
||||
)
|
||||
conversationDao.insertConversation(conversation)
|
||||
Result.success(response.id)
|
||||
},
|
||||
onFailure = { exception ->
|
||||
Result.failure(exception)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 从API同步对话列表
|
||||
*/
|
||||
suspend fun syncConversations() {
|
||||
val result = apiService.getConversationList()
|
||||
result.getOrNull()?.let { conversations ->
|
||||
// 将DTO转换为Entity并保存到数据库
|
||||
conversations.forEach { dto ->
|
||||
val conversation = Conversation(
|
||||
id = dto.id,
|
||||
userId = "", // 需要从TokenManager获取
|
||||
startedAt = dto.latestMessageTime,
|
||||
endedAt = null,
|
||||
durationSeconds = 0,
|
||||
summary = dto.latestMessagePreview,
|
||||
currentTopic = null,
|
||||
conversationStage = null,
|
||||
avatarUrl = dto.avatarUrl,
|
||||
title = dto.title,
|
||||
latestMessagePreview = dto.latestMessagePreview,
|
||||
latestMessageTime = dto.latestMessageTime
|
||||
)
|
||||
conversationDao.insertConversation(conversation)
|
||||
result.fold(
|
||||
onSuccess = { conversations ->
|
||||
// 将DTO转换为Entity并保存到数据库
|
||||
conversations.forEach { dto ->
|
||||
val conversation = Conversation(
|
||||
id = dto.id,
|
||||
userId = "", // 需要从TokenManager获取
|
||||
startedAt = dto.latestMessageTime,
|
||||
endedAt = null,
|
||||
durationSeconds = 0,
|
||||
summary = dto.latestMessagePreview,
|
||||
currentTopic = null,
|
||||
conversationStage = null,
|
||||
avatarUrl = dto.avatarUrl,
|
||||
title = dto.title,
|
||||
latestMessagePreview = dto.latestMessagePreview,
|
||||
latestMessageTime = dto.latestMessageTime
|
||||
)
|
||||
conversationDao.insertConversation(conversation)
|
||||
}
|
||||
},
|
||||
onFailure = { exception ->
|
||||
// 失败时抛出异常,让调用方处理
|
||||
throw exception
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,184 @@
|
||||
package com.huaga.life_echo.ui.components.debug
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.huaga.life_echo.ui.icons.AppIcons
|
||||
|
||||
/**
|
||||
* WebSocket调试面板 - 开发测试用
|
||||
* 实时显示WebSocket连接状态、消息收发情况等调试信息
|
||||
*/
|
||||
@Composable
|
||||
fun WebSocketDebugPanel(
|
||||
connectionStatus: String,
|
||||
conversationId: String?,
|
||||
isConnected: Boolean,
|
||||
isStreaming: Boolean,
|
||||
isTyping: Boolean,
|
||||
lastMessageType: String?,
|
||||
lastMessageTime: String?,
|
||||
errorMessages: List<String>,
|
||||
messageCount: Int,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
var isExpanded by remember { mutableStateOf(false) }
|
||||
|
||||
// 根据连接状态决定颜色
|
||||
val statusColor = when {
|
||||
connectionStatus.contains("已连接") -> Color(0xFF4CAF50)
|
||||
connectionStatus.contains("连接中") -> Color(0xFFFF9800)
|
||||
connectionStatus.contains("错误") || connectionStatus.contains("失败") -> Color(0xFFF44336)
|
||||
else -> Color(0xFF9E9E9E)
|
||||
}
|
||||
|
||||
Card(
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
shape = RoundedCornerShape(8.dp),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.9f)
|
||||
),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(12.dp)
|
||||
) {
|
||||
// 标题栏(可点击展开/收起)
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable { isExpanded = !isExpanded },
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
// 状态指示器
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(12.dp)
|
||||
.background(
|
||||
color = statusColor,
|
||||
shape = RoundedCornerShape(50)
|
||||
)
|
||||
)
|
||||
Text(
|
||||
text = "WS调试",
|
||||
fontSize = 14.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
Text(
|
||||
text = connectionStatus,
|
||||
fontSize = 12.sp,
|
||||
color = statusColor
|
||||
)
|
||||
}
|
||||
|
||||
Icon(
|
||||
imageVector = if (isExpanded) AppIcons.ExpandLess else AppIcons.ExpandMore,
|
||||
contentDescription = if (isExpanded) "收起" else "展开",
|
||||
modifier = Modifier.size(20.dp),
|
||||
tint = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
|
||||
// 详细信息(展开时显示)
|
||||
AnimatedVisibility(visible = isExpanded) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(top = 12.dp)
|
||||
.verticalScroll(rememberScrollState()),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
// 基本信息
|
||||
DebugInfoRow("连接状态", connectionStatus, statusColor)
|
||||
DebugInfoRow("是否已连接", if (isConnected) "是" else "否", if (isConnected) Color(0xFF4CAF50) else Color(0xFFF44336))
|
||||
DebugInfoRow("对话ID", conversationId ?: "未设置", MaterialTheme.colorScheme.onSurfaceVariant)
|
||||
|
||||
Divider(modifier = Modifier.padding(vertical = 4.dp))
|
||||
|
||||
// 消息统计
|
||||
DebugInfoRow("消息总数", "$messageCount", MaterialTheme.colorScheme.primary)
|
||||
DebugInfoRow("流式状态", if (isStreaming) "正在接收" else "空闲", if (isStreaming) Color(0xFFFF9800) else Color(0xFF4CAF50))
|
||||
DebugInfoRow("输入状态", if (isTyping) "AI正在输入" else "空闲", if (isTyping) Color(0xFFFF9800) else Color(0xFF4CAF50))
|
||||
|
||||
// 最后一条消息信息
|
||||
if (lastMessageType != null) {
|
||||
Divider(modifier = Modifier.padding(vertical = 4.dp))
|
||||
DebugInfoRow("最后消息类型", lastMessageType, MaterialTheme.colorScheme.primary)
|
||||
if (lastMessageTime != null) {
|
||||
DebugInfoRow("最后消息时间", lastMessageTime, MaterialTheme.colorScheme.onSurfaceVariant)
|
||||
}
|
||||
}
|
||||
|
||||
// 错误信息
|
||||
if (errorMessages.isNotEmpty()) {
|
||||
Divider(modifier = Modifier.padding(vertical = 4.dp))
|
||||
Text(
|
||||
text = "错误日志",
|
||||
fontSize = 12.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = Color(0xFFF44336),
|
||||
modifier = Modifier.padding(bottom = 4.dp)
|
||||
)
|
||||
errorMessages.takeLast(5).forEach { error ->
|
||||
Text(
|
||||
text = "• $error",
|
||||
fontSize = 10.sp,
|
||||
color = Color(0xFFF44336),
|
||||
fontFamily = FontFamily.Monospace,
|
||||
modifier = Modifier.padding(start = 8.dp, bottom = 2.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DebugInfoRow(
|
||||
label: String,
|
||||
value: String,
|
||||
valueColor: Color
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
text = "$label:",
|
||||
fontSize = 11.sp,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
fontWeight = FontWeight.Medium
|
||||
)
|
||||
Text(
|
||||
text = value,
|
||||
fontSize = 11.sp,
|
||||
color = valueColor,
|
||||
fontWeight = FontWeight.Bold,
|
||||
modifier = Modifier.padding(start = 8.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
package com.huaga.life_echo.ui.components.memoir
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
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.compose.ui.window.Dialog
|
||||
import com.huaga.life_echo.network.models.ConversationListItemDto
|
||||
import com.huaga.life_echo.ui.theme.LightPurple
|
||||
import com.huaga.life_echo.utils.TimeUtils
|
||||
|
||||
/**
|
||||
* 整理对话对话框
|
||||
* 用于选择要整理的对话
|
||||
*/
|
||||
@Composable
|
||||
fun OrganizeConversationDialog(
|
||||
conversations: List<ConversationListItemDto>,
|
||||
isLoading: Boolean = false,
|
||||
onDismiss: () -> Unit,
|
||||
onSelectConversation: (String) -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Dialog(onDismissRequest = onDismiss) {
|
||||
Card(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.fillMaxHeight(0.8f),
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = MaterialTheme.colorScheme.surface
|
||||
)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxSize()
|
||||
) {
|
||||
// 标题栏
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
text = "选择要整理的对话",
|
||||
fontSize = 18.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
IconButton(onClick = onDismiss) {
|
||||
Text("✕", fontSize = 20.sp, color = MaterialTheme.colorScheme.onSurface)
|
||||
}
|
||||
}
|
||||
|
||||
Divider()
|
||||
|
||||
// 对话列表
|
||||
if (isLoading) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.weight(1f),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
CircularProgressIndicator(color = LightPurple)
|
||||
}
|
||||
} else if (conversations.isEmpty()) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.weight(1f),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(
|
||||
text = "还没有对话",
|
||||
fontSize = 14.sp,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
} else {
|
||||
LazyColumn(
|
||||
modifier = Modifier.weight(1f),
|
||||
contentPadding = PaddingValues(vertical = 8.dp)
|
||||
) {
|
||||
items(conversations, key = { it.id }) { conversation ->
|
||||
ConversationItem(
|
||||
conversation = conversation,
|
||||
onClick = {
|
||||
onSelectConversation(conversation.id)
|
||||
onDismiss()
|
||||
},
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp, vertical = 4.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ConversationItem(
|
||||
conversation: ConversationListItemDto,
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Card(
|
||||
modifier = modifier.clickable { onClick() },
|
||||
shape = RoundedCornerShape(12.dp),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f)
|
||||
),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = 1.dp)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp)
|
||||
) {
|
||||
Text(
|
||||
text = conversation.title,
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.Medium,
|
||||
color = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
if (conversation.latestMessagePreview != null) {
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
Text(
|
||||
text = conversation.latestMessagePreview,
|
||||
fontSize = 14.sp,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
maxLines = 2
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Text(
|
||||
text = TimeUtils.formatDateTime(conversation.latestMessageTime),
|
||||
fontSize = 12.sp,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
20
app-android/app/src/main/res/xml/network_security_config.xml
Normal file
20
app-android/app/src/main/res/xml/network_security_config.xml
Normal file
@@ -0,0 +1,20 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<network-security-config>
|
||||
<!-- 开发环境:允许明文HTTP流量 -->
|
||||
<!-- 注意:生产环境应该移除此配置,强制使用HTTPS -->
|
||||
<base-config cleartextTrafficPermitted="true">
|
||||
<trust-anchors>
|
||||
<certificates src="system" />
|
||||
</trust-anchors>
|
||||
</base-config>
|
||||
|
||||
<!-- 可选:仅允许特定域名的明文流量(更安全的方式) -->
|
||||
<!--
|
||||
<domain-config cleartextTrafficPermitted="true">
|
||||
<domain includeSubdomains="true">192.168.10.9</domain>
|
||||
<domain includeSubdomains="true">10.0.2.2</domain>
|
||||
<domain includeSubdomains="true">localhost</domain>
|
||||
<domain includeSubdomains="true">127.0.0.1</domain>
|
||||
</domain-config>
|
||||
-->
|
||||
</network-security-config>
|
||||
@@ -16,6 +16,7 @@ navigationCompose = "2.8.4"
|
||||
permissions = "0.34.0"
|
||||
coroutines = "1.9.0"
|
||||
serialization = "1.7.3"
|
||||
coil = "2.5.0"
|
||||
|
||||
[libraries]
|
||||
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
|
||||
@@ -38,6 +39,7 @@ androidx-compose-material3-adaptive-navigation-suite = { group = "androidx.compo
|
||||
# Ktor
|
||||
ktor-client-core = { group = "io.ktor", name = "ktor-client-core", version.ref = "ktor" }
|
||||
ktor-client-android = { group = "io.ktor", name = "ktor-client-android", version.ref = "ktor" }
|
||||
ktor-client-okhttp = { group = "io.ktor", name = "ktor-client-okhttp", version.ref = "ktor" }
|
||||
ktor-client-websockets = { group = "io.ktor", name = "ktor-client-websockets", version.ref = "ktor" }
|
||||
ktor-client-content-negotiation = { group = "io.ktor", name = "ktor-client-content-negotiation", version.ref = "ktor" }
|
||||
ktor-serialization-kotlinx-json = { group = "io.ktor", name = "ktor-serialization-kotlinx-json", version.ref = "ktor" }
|
||||
@@ -61,6 +63,9 @@ kotlinx-coroutines-android = { group = "org.jetbrains.kotlinx", name = "kotlinx-
|
||||
# Serialization
|
||||
kotlinx-serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "serialization" }
|
||||
|
||||
# Image Loading
|
||||
coil-compose = { group = "io.coil-kt", name = "coil-compose", version.ref = "coil" }
|
||||
|
||||
[plugins]
|
||||
android-application = { id = "com.android.application", version.ref = "agp" }
|
||||
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
|
||||
|
||||
Reference in New Issue
Block a user