自定义NavigationItem,新增群组创建页面

This commit is contained in:
weber
2025-08-12 19:06:56 +08:00
parent 697af504b7
commit 2d518cbd68
18 changed files with 941 additions and 104 deletions

View File

@@ -17,6 +17,7 @@ open class AppThemeData(
var loadingText: Color, var loadingText: Color,
var disabledBackground: Color, var disabledBackground: Color,
var background: Color, var background: Color,
var secondaryBackground: Color,
var decentBackground: Color, var decentBackground: Color,
var divider: Color, var divider: Color,
var inputBackground: Color, var inputBackground: Color,
@@ -46,6 +47,7 @@ class LightThemeColors : AppThemeData(
loadingText = Color(0xffffffff), loadingText = Color(0xffffffff),
disabledBackground = Color(0xFFD0D0D0), disabledBackground = Color(0xFFD0D0D0),
background = Color(0xFFFFFFFF), background = Color(0xFFFFFFFF),
secondaryBackground = Color(0xFFF7f7f7),
divider = Color(0xFFEbEbEb), divider = Color(0xFFEbEbEb),
inputBackground = Color(0xFFF7f7f7), inputBackground = Color(0xFFF7f7f7),
inputBackground2 = Color(0xFFFFFFFF), inputBackground2 = Color(0xFFFFFFFF),
@@ -76,6 +78,7 @@ class DarkThemeColors : AppThemeData(
loadingText = Color(0xff000000), loadingText = Color(0xff000000),
disabledBackground = Color(0xFF3A3A3A), disabledBackground = Color(0xFF3A3A3A),
background = Color(0xFF121212), background = Color(0xFF121212),
secondaryBackground = Color(0xFF1C1C1C),
divider = Color(0xFF282828), divider = Color(0xFF282828),
inputBackground = Color(0xFF1C1C1C), inputBackground = Color(0xFF1C1C1C),
inputBackground2 = Color(0xFF1C1C1C), inputBackground2 = Color(0xFF1C1C1C),

View File

@@ -32,6 +32,7 @@ import com.aiosman.ravenow.ui.account.AccountEditScreen2
import com.aiosman.ravenow.ui.account.AccountSetting import com.aiosman.ravenow.ui.account.AccountSetting
import com.aiosman.ravenow.ui.account.ResetPasswordScreen import com.aiosman.ravenow.ui.account.ResetPasswordScreen
import com.aiosman.ravenow.ui.agent.AddAgentScreen import com.aiosman.ravenow.ui.agent.AddAgentScreen
import com.aiosman.ravenow.ui.group.CreateGroupChatScreen
import com.aiosman.ravenow.ui.chat.ChatAiScreen import com.aiosman.ravenow.ui.chat.ChatAiScreen
import com.aiosman.ravenow.ui.chat.ChatScreen import com.aiosman.ravenow.ui.chat.ChatScreen
import com.aiosman.ravenow.ui.comment.CommentsScreen import com.aiosman.ravenow.ui.comment.CommentsScreen
@@ -98,6 +99,7 @@ sealed class NavigationRoute(
data object AccountSetting : NavigationRoute("AccountSetting") data object AccountSetting : NavigationRoute("AccountSetting")
data object AboutScreen : NavigationRoute("AboutScreen") data object AboutScreen : NavigationRoute("AboutScreen")
data object AddAgent : NavigationRoute("AddAgent") data object AddAgent : NavigationRoute("AddAgent")
data object CreateGroupChat : NavigationRoute("CreateGroupChat")
} }
@@ -424,6 +426,18 @@ fun NavigationController(
AddAgentScreen() AddAgentScreen()
} }
composable(
route = NavigationRoute.CreateGroupChat.route,
enterTransition = {
fadeIn(animationSpec = tween(durationMillis = 0))
},
exitTransition = {
fadeOut(animationSpec = tween(durationMillis = 0))
}
) {
CreateGroupChatScreen()
}
} }
} }

View File

@@ -30,12 +30,14 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.aiosman.ravenow.AppState
import com.aiosman.ravenow.LocalAppTheme import com.aiosman.ravenow.LocalAppTheme
import com.aiosman.ravenow.LocalNavController import com.aiosman.ravenow.LocalNavController
import com.aiosman.ravenow.R import com.aiosman.ravenow.R
import com.aiosman.ravenow.ui.NavigationRoute import com.aiosman.ravenow.ui.NavigationRoute
import com.aiosman.ravenow.ui.comment.NoticeScreenHeader import com.aiosman.ravenow.ui.comment.NoticeScreenHeader
import com.aiosman.ravenow.ui.composables.CustomAsyncImage import com.aiosman.ravenow.ui.composables.CustomAsyncImage
import com.aiosman.ravenow.ui.composables.StatusBarMaskLayout
import com.aiosman.ravenow.ui.composables.StatusBarSpacer import com.aiosman.ravenow.ui.composables.StatusBarSpacer
import com.aiosman.ravenow.ui.composables.form.FormTextInput import com.aiosman.ravenow.ui.composables.form.FormTextInput
import com.aiosman.ravenow.ui.modifiers.noRippleClickable import com.aiosman.ravenow.ui.modifiers.noRippleClickable
@@ -82,13 +84,18 @@ fun AccountEditScreen2() {
} }
} }
StatusBarMaskLayout(
modifier = Modifier.background(color = appColors.background).padding(horizontal = 16.dp),
darkIcons = !AppState.darkMode,
maskBoxBackgroundColor = appColors.background
) {
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.background(color = appColors.background), .background(color = appColors.background),
horizontalAlignment = Alignment.CenterHorizontally horizontalAlignment = Alignment.CenterHorizontally
) { ) {
StatusBarSpacer() //StatusBarSpacer()
Box( Box(
modifier = Modifier.padding(horizontal = 24.dp, vertical = 16.dp) modifier = Modifier.padding(horizontal = 24.dp, vertical = 16.dp)
) { ) {
@@ -184,7 +191,7 @@ fun AccountEditScreen2() {
} }
} }
} }}
} }

View File

@@ -0,0 +1,70 @@
package com.aiosman.ravenow.ui.group
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.aiosman.ravenow.LocalAppTheme
import com.aiosman.ravenow.ui.composables.CustomAsyncImage
import com.aiosman.ravenow.ui.modifiers.noRippleClickable
@Composable
fun AiAgentListScreen(
searchText: String,
selectedMemberIds: Set<String> = emptySet(),
onMemberSelect: (GroupMember) -> Unit
) {
val AppColors = LocalAppTheme.current
val context = LocalContext.current
var aiAgents by remember { mutableStateOf<List<GroupMember>>(emptyList()) }
var isLoading by remember { mutableStateOf(false) }
// 加载AI智能体数据
LaunchedEffect(Unit) {
isLoading = true
aiAgents = CreateGroupChatViewModel.getAiAgents()
isLoading = false
}
val filteredAgents = if (searchText.isEmpty()) {
aiAgents
} else {
aiAgents.filter { it.name.contains(searchText, ignoreCase = true) }
}
if (isLoading) {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Text(
text = "加载中...",
color = AppColors.secondaryText,
fontSize = 14.sp
)
}
} else {
LazyColumn(
modifier = Modifier.fillMaxSize(),
contentPadding = PaddingValues(16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
items(filteredAgents) { agent ->
MemberItem(
member = agent,
isSelected = selectedMemberIds.contains(agent.id),
onSelect = { onMemberSelect(agent) }
)
}
}
}
}

View File

@@ -0,0 +1,397 @@
package com.aiosman.ravenow.ui.group
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.aiosman.ravenow.LocalAppTheme
import com.aiosman.ravenow.LocalNavController
import com.aiosman.ravenow.R
import com.aiosman.ravenow.ui.composables.CustomAsyncImage
import com.aiosman.ravenow.ui.composables.StatusBarSpacer
import com.aiosman.ravenow.ui.composables.TabItem
import com.aiosman.ravenow.ui.composables.TabSpacer
import com.aiosman.ravenow.ui.modifiers.noRippleClickable
import com.google.accompanist.systemuicontroller.rememberSystemUiController
import kotlinx.coroutines.launch
// 成员数据类
data class GroupMember(
val id: String,
val name: String,
val avatar: String
)
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun CreateGroupChatScreen() {
val AppColors = LocalAppTheme.current
val navController = LocalNavController.current
val systemUiController = rememberSystemUiController()
val context = LocalContext.current
// 状态管理
var groupName by remember { mutableStateOf(TextFieldValue("")) }
var searchText by remember { mutableStateOf(TextFieldValue("")) }
var selectedMembers by remember { mutableStateOf(listOf<GroupMember>()) }
var selectedMemberIds by remember { mutableStateOf<Set<String>>(emptySet()) }
var pagerState = rememberPagerState(pageCount = { 2 })
var scope = rememberCoroutineScope()
// 清除错误信息
LaunchedEffect(groupName.text, searchText.text) {
if (CreateGroupChatViewModel.errorMessage != null) {
CreateGroupChatViewModel.clearError()
}
}
// 监听页面切换清除当前tab的选中状态但保留已选成员列表
LaunchedEffect(pagerState.currentPage) {
// 不清除selectedMemberIds因为我们需要保持跨tab的选中状态
// 这样用户可以在AI智能体和朋友之间切换选中的状态会保持
}
val navigationBarPaddings = WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() + 48.dp
LaunchedEffect(Unit) {
systemUiController.setNavigationBarColor(Color.Transparent)
}
Column(
modifier = Modifier
.fillMaxSize()
.padding(bottom = navigationBarPaddings)
) {
// 错误提示
CreateGroupChatViewModel.errorMessage?.let { error ->
Box(
modifier = Modifier
.fillMaxWidth()
.background(AppColors.error.copy(alpha = 0.1f))
.padding(16.dp)
) {
Text(
text = error,
color = AppColors.error,
fontSize = 14.sp
)
}
}
StatusBarSpacer()
// 头部
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 12.dp),
verticalAlignment = Alignment.CenterVertically
) {
// 返回按钮
Image(
painter = painterResource(id = R.drawable.rider_pro_back_icon),
contentDescription = "back",
modifier = Modifier
.size(24.dp)
.noRippleClickable {
navController.popBackStack()
},
colorFilter = ColorFilter.tint(AppColors.text)
)
// 标题
Text(
text = stringResource(R.string.create_group_chat),
fontSize = 17.sp,
fontWeight = FontWeight.W700,
color = AppColors.text,
modifier = Modifier
.weight(1f)
.padding(start = 16.dp)
)
// 一键创建按钮
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.noRippleClickable {
// 一键创建逻辑
}
) {
Image(
painter = painterResource(id = R.drawable.rider_pro_new_post_add_pic),
contentDescription = "quick create",
modifier = Modifier.size(16.dp),
colorFilter = ColorFilter.tint(AppColors.main)
)
Spacer(modifier = Modifier.width(4.dp))
Text(
text = stringResource(R.string.quick_create),
fontSize = 14.sp,
color = AppColors.main
)
}
}
// 搜索栏
Box(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 8.dp)
.background(
color = AppColors.inputBackground,
shape = RoundedCornerShape(8.dp)
)
.padding(horizontal = 12.dp, vertical = 8.dp)
) {
BasicTextField(
value = searchText,
onValueChange = { searchText = it },
textStyle = androidx.compose.ui.text.TextStyle(
color = AppColors.text,
fontSize = 14.sp
),
modifier = Modifier.fillMaxWidth(),
decorationBox = { innerTextField ->
Row(
verticalAlignment = Alignment.CenterVertically
) {
Image(
painter = painterResource(id = R.drawable.rider_pro_nav_search),
contentDescription = "search",
modifier = Modifier.size(16.dp),
colorFilter = ColorFilter.tint(AppColors.secondaryText)
)
Spacer(modifier = Modifier.width(8.dp))
if (searchText.text.isEmpty()) {
Text(
text = "搜索",
color = AppColors.secondaryText,
fontSize = 14.sp
)
}
innerTextField()
}
}
)
}
// 群聊名称输入框
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 8.dp),
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = "群聊名称",
fontSize = 14.sp,
color = AppColors.text,
modifier = Modifier.width(80.dp)
)
BasicTextField(
value = groupName,
onValueChange = { groupName = it },
textStyle = androidx.compose.ui.text.TextStyle(
color = AppColors.text,
fontSize = 14.sp
),
modifier = Modifier
.weight(1f)
.background(
color = AppColors.inputBackground,
shape = RoundedCornerShape(8.dp)
)
.padding(horizontal = 12.dp, vertical = 8.dp)
)
}
// 已选成员列表
if (selectedMembers.isNotEmpty()) {
// 显示选中成员数量
Text(
text = "已选择 ${selectedMembers.size} 个成员",
fontSize = 14.sp,
color = AppColors.secondaryText,
modifier = Modifier.padding(horizontal = 16.dp, vertical = 4.dp)
)
LazyRow(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 8.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
items(selectedMembers) { member ->
Box {
CustomAsyncImage(
context = context,
imageUrl = member.avatar,
contentDescription = member.name,
modifier = Modifier
.size(48.dp)
.clip(CircleShape)
)
// 删除按钮
Box(
modifier = Modifier
.size(20.dp)
.background(AppColors.error, CircleShape)
.align(Alignment.TopEnd)
.noRippleClickable {
// 删除成员时同时更新选中状态
val (newSelectedMemberIds, newSelectedMembers) = CreateGroupChatViewModel.removeSelectedMember(
member, selectedMemberIds, selectedMembers
)
selectedMemberIds = newSelectedMemberIds
selectedMembers = newSelectedMembers
},
contentAlignment = Alignment.Center
) {
Text(
text = "×",
color = Color.White,
fontSize = 14.sp,
fontWeight = FontWeight.Bold
)
}
}
}
}
}
// Tab切换
Row(
modifier = Modifier
.fillMaxWidth()
.wrapContentHeight()
.padding(horizontal = 16.dp, vertical = 8.dp),
horizontalArrangement = Arrangement.Start,
verticalAlignment = Alignment.Bottom
) {
TabItem(
text = stringResource(R.string.chat_ai),
isSelected = pagerState.currentPage == 0,
onClick = {
scope.launch {
pagerState.animateScrollToPage(0)
}
}
)
TabSpacer()
TabItem(
text = stringResource(R.string.chat_friend),
isSelected = pagerState.currentPage == 1,
onClick = {
scope.launch {
pagerState.animateScrollToPage(1)
}
}
)
}
// 内容区域
HorizontalPager(
state = pagerState,
modifier = Modifier
.fillMaxWidth()
.weight(1f)
) {
when (it) {
0 -> {
// AI智能体列表
AiAgentListScreen(
searchText = searchText.text,
selectedMemberIds = selectedMemberIds,
onMemberSelect = { member ->
val (newSelectedMemberIds, newSelectedMembers) = CreateGroupChatViewModel.toggleMemberSelection(
member, selectedMemberIds, selectedMembers
)
selectedMemberIds = newSelectedMemberIds
selectedMembers = newSelectedMembers
}
)
}
1 -> {
// 朋友列表
FriendListScreen(
searchText = searchText.text,
selectedMemberIds = selectedMemberIds,
onMemberSelect = { member ->
val (newSelectedMemberIds, newSelectedMembers) = CreateGroupChatViewModel.toggleMemberSelection(
member, selectedMemberIds, selectedMembers
)
selectedMemberIds = newSelectedMemberIds
selectedMembers = newSelectedMembers
}
)
}
}
}
// 创建群聊按钮
Button(
onClick = {
// 创建群聊逻辑
if (groupName.text.isNotEmpty() && selectedMembers.isNotEmpty()) {
scope.launch {
val success = CreateGroupChatViewModel.createGroupChat(
groupName = groupName.text,
memberIds = selectedMembers.map { it.id },
context = context
)
if (success) {
navController.popBackStack()
}
}
}
},
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 16.dp),
colors = ButtonDefaults.buttonColors(
containerColor = AppColors.main,
contentColor = AppColors.mainText
),
shape = RoundedCornerShape(8.dp),
enabled = groupName.text.isNotEmpty() && selectedMembers.isNotEmpty() && !CreateGroupChatViewModel.isLoading
) {
if (CreateGroupChatViewModel.isLoading) {
Text(
text = "创建中...",
fontSize = 16.sp,
fontWeight = FontWeight.W600
)
} else {
Text(
text = stringResource(R.string.create_group_chat),
fontSize = 16.sp,
fontWeight = FontWeight.W600
)
}
}
}
}

View File

@@ -0,0 +1,133 @@
package com.aiosman.ravenow.ui.group
import android.content.Context
import android.icu.util.Calendar
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.navigation.NavController
import androidx.navigation.NavHostController
import androidx.paging.PagingData
import androidx.paging.map
import com.aiosman.ravenow.AppState
import com.aiosman.ravenow.ConstVars
import com.aiosman.ravenow.data.AccountNotice
import com.aiosman.ravenow.data.AccountService
import com.aiosman.ravenow.data.AccountServiceImpl
import com.aiosman.ravenow.data.UserService
import com.aiosman.ravenow.data.UserServiceImpl
import com.aiosman.ravenow.entity.CommentEntity
import com.aiosman.ravenow.exp.formatChatTime
import com.aiosman.ravenow.ui.NavigationRoute
import com.aiosman.ravenow.ui.navigateToChat
import com.aiosman.ravenow.utils.TrtcHelper
import com.tencent.imsdk.v2.V2TIMConversation
import com.tencent.imsdk.v2.V2TIMConversationResult
import com.tencent.imsdk.v2.V2TIMManager
import com.tencent.imsdk.v2.V2TIMMessage
import com.tencent.imsdk.v2.V2TIMValueCallback
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch
import kotlin.coroutines.suspendCoroutine
object CreateGroupChatViewModel : ViewModel() {
val accountService: AccountService = AccountServiceImpl()
val userService: UserService = UserServiceImpl()
// 状态管理
var isLoading by mutableStateOf(false)
var errorMessage by mutableStateOf<String?>(null)
// 获取AI智能体列表
suspend fun getAiAgents(): List<GroupMember> {
return try {
// TODO: 从API获取AI智能体列表
listOf(
GroupMember("1", "AI助手", "https://example.com/avatar1.jpg"),
GroupMember("2", "智能客服", "https://example.com/avatar2.jpg"),
GroupMember("3", "翻译助手", "https://example.com/avatar3.jpg"),
GroupMember("4", "写作助手", "https://example.com/avatar4.jpg"),
GroupMember("5", "编程助手", "https://example.com/avatar5.jpg"),
GroupMember("6", "设计助手", "https://example.com/avatar6.jpg")
)
} catch (e: Exception) {
errorMessage = "获取AI智能体列表失败: ${e.message}"
emptyList()
}
}
// 获取朋友列表
suspend fun getFriends(): List<GroupMember> {
return try {
// TODO: 从API获取朋友列表
listOf(
GroupMember("7", "张三", "https://example.com/avatar7.jpg"),
GroupMember("8", "李四", "https://example.com/avatar8.jpg"),
GroupMember("9", "王五", "https://example.com/avatar9.jpg"),
GroupMember("10", "赵六", "https://example.com/avatar10.jpg"),
GroupMember("11", "钱七", "https://example.com/avatar11.jpg"),
GroupMember("12", "孙八", "https://example.com/avatar12.jpg")
)
} catch (e: Exception) {
errorMessage = "获取朋友列表失败: ${e.message}"
emptyList()
}
}
// 创建群聊
suspend fun createGroupChat(
groupName: String,
memberIds: List<String>,
context: Context
): Boolean {
return try {
isLoading = true
// TODO: 实现创建群聊的API调用
// 这里应该调用实际的API来创建群聊
// 模拟API调用延迟
kotlinx.coroutines.delay(1000)
isLoading = false
true
} catch (e: Exception) {
isLoading = false
errorMessage = "创建群聊失败: ${e.message}"
false
}
}
// 清除错误信息
fun clearError() {
errorMessage = null
}
// 添加成员到选中列表
fun addSelectedMember(member: GroupMember, selectedMemberIds: Set<String>, selectedMembers: List<GroupMember>): Pair<Set<String>, List<GroupMember>> {
val newSelectedMemberIds = selectedMemberIds + member.id
val newSelectedMembers = if (selectedMembers.none { it.id == member.id }) {
selectedMembers + member
} else {
selectedMembers
}
return Pair(newSelectedMemberIds, newSelectedMembers)
}
// 从选中列表移除成员
fun removeSelectedMember(member: GroupMember, selectedMemberIds: Set<String>, selectedMembers: List<GroupMember>): Pair<Set<String>, List<GroupMember>> {
val newSelectedMemberIds = selectedMemberIds - member.id
val newSelectedMembers = selectedMembers.filter { it.id != member.id }
return Pair(newSelectedMemberIds, newSelectedMembers)
}
// 切换成员选中状态
fun toggleMemberSelection(member: GroupMember, selectedMemberIds: Set<String>, selectedMembers: List<GroupMember>): Pair<Set<String>, List<GroupMember>> {
return if (selectedMemberIds.contains(member.id)) {
removeSelectedMember(member, selectedMemberIds, selectedMembers)
} else {
addSelectedMember(member, selectedMemberIds, selectedMembers)
}
}
}

View File

@@ -0,0 +1,70 @@
package com.aiosman.ravenow.ui.group
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.aiosman.ravenow.LocalAppTheme
import com.aiosman.ravenow.ui.composables.CustomAsyncImage
import com.aiosman.ravenow.ui.modifiers.noRippleClickable
@Composable
fun FriendListScreen(
searchText: String,
selectedMemberIds: Set<String> = emptySet(),
onMemberSelect: (GroupMember) -> Unit
) {
val AppColors = LocalAppTheme.current
val context = LocalContext.current
var friends by remember { mutableStateOf<List<GroupMember>>(emptyList()) }
var isLoading by remember { mutableStateOf(false) }
// 加载朋友数据
LaunchedEffect(Unit) {
isLoading = true
friends = CreateGroupChatViewModel.getFriends()
isLoading = false
}
val filteredFriends = if (searchText.isEmpty()) {
friends
} else {
friends.filter { it.name.contains(searchText, ignoreCase = true) }
}
if (isLoading) {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Text(
text = "加载中...",
color = AppColors.secondaryText,
fontSize = 14.sp
)
}
} else {
LazyColumn(
modifier = Modifier.fillMaxSize(),
contentPadding = PaddingValues(16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
items(filteredFriends) { friend ->
MemberItem(
member = friend,
isSelected = selectedMemberIds.contains(friend.id),
onSelect = { onMemberSelect(friend) }
)
}
}
}
}

View File

@@ -0,0 +1,65 @@
package com.aiosman.ravenow.ui.group
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.Checkbox
import androidx.compose.material3.CheckboxDefaults
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.aiosman.ravenow.LocalAppTheme
import com.aiosman.ravenow.ui.composables.CustomAsyncImage
import com.aiosman.ravenow.ui.modifiers.noRippleClickable
@Composable
fun MemberItem(
member: GroupMember,
isSelected: Boolean = false,
onSelect: () -> Unit
) {
val AppColors = LocalAppTheme.current
val context = LocalContext.current
Row(
modifier = Modifier
.fillMaxWidth()
.noRippleClickable { onSelect() }
.padding(vertical = 8.dp),
verticalAlignment = Alignment.CenterVertically
) {
CustomAsyncImage(
context = context,
imageUrl = member.avatar,
contentDescription = member.name,
modifier = Modifier
.size(40.dp)
.clip(CircleShape)
)
Spacer(modifier = Modifier.width(12.dp))
Text(
text = member.name,
fontSize = 16.sp,
color = AppColors.text,
modifier = Modifier.weight(1f)
)
Spacer(modifier = Modifier.width(8.dp))
Checkbox(
checked = isSelected,
onCheckedChange = { onSelect() },
colors = CheckboxDefaults.colors(
checkedColor = AppColors.main,
uncheckedColor = AppColors.secondaryText
)
)
}
}

View File

@@ -52,6 +52,7 @@ import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
@@ -259,7 +260,7 @@ fun IndexScreen() {
Scaffold( Scaffold(
bottomBar = { bottomBar = {
NavigationBar( NavigationBar(
modifier = Modifier.height(56.dp + navigationBarHeight), modifier = Modifier.height(72.dp + navigationBarHeight),
containerColor = AppColors.background containerColor = AppColors.background
) { ) {
item.forEachIndexed { idx, it -> item.forEachIndexed { idx, it ->
@@ -268,6 +269,7 @@ fun IndexScreen() {
targetValue = if (isSelected) AppColors.brandColorsColor else AppColors.text, targetValue = if (isSelected) AppColors.brandColorsColor else AppColors.text,
animationSpec = tween(durationMillis = 250), label = "" animationSpec = tween(durationMillis = 250), label = ""
) )
NavigationBarItem( NavigationBarItem(
modifier = Modifier.padding(top = 6.dp), modifier = Modifier.padding(top = 6.dp),
selected = isSelected, selected = isSelected,
@@ -282,58 +284,65 @@ fun IndexScreen() {
} }
model.tabIndex = idx model.tabIndex = idx
}, },
interactionSource = remember { MutableInteractionSource() },
colors = NavigationBarItemColors( colors = NavigationBarItemColors(
selectedTextColor = Color.Red, selectedIconColor = Color.Transparent,
selectedTextColor = Color.Transparent,
selectedIndicatorColor = Color.Transparent, selectedIndicatorColor = Color.Transparent,
unselectedTextColor = Color.Red, unselectedIconColor = Color.Transparent,
disabledIconColor = Color.Red, unselectedTextColor = Color.Transparent,
disabledTextColor = Color.Red, disabledIconColor = Color.Transparent,
selectedIconColor = iconTint, disabledTextColor = Color.Transparent
unselectedIconColor = iconTint,
), ),
icon = { icon = {
// 特殊处理中间的Add按钮只显示图标并放大 Column(
if (it.route == NavigationItem.Add.route) { horizontalAlignment = Alignment.CenterHorizontally,
Icon( verticalArrangement = Arrangement.Center
modifier = Modifier.size(32.dp), ) {
imageVector = if (isSelected) it.selectedIcon() else it.icon(), if (it.route == NavigationItem.Add.route) {
contentDescription = null, // Add按钮只显示大图标
tint = AppColors.text Image(
) painter = painterResource(if (isSelected) it.selectedIcon() else it.icon()),
} else { contentDescription = it.label(),
Box( modifier = Modifier.size(32.dp),
modifier = Modifier colorFilter = if (!isSelected) ColorFilter.tint(AppColors.text) else null
.width(46.dp) )
.height(30.dp) } else {
.background( // 其他按钮:图标+文字
color = if (isSelected) AppColors.brandColorsColor.copy(alpha = 0.1f) else Color.Transparent , Box(
shape = RoundedCornerShape(10.dp) modifier = Modifier
), .width(48.dp)
contentAlignment = Alignment.Center .height(32.dp)
) { .background(
Icon( color = if (isSelected) AppColors.brandColorsColor.copy(alpha = 0.15f) else Color.Transparent,
modifier = Modifier.size(24.dp), shape = RoundedCornerShape(12.dp)
imageVector = if (isSelected) it.selectedIcon() else it.icon(), ),
contentDescription = null, contentAlignment = Alignment.Center
tint = iconTint ) {
Image(
painter = painterResource(if (isSelected) it.selectedIcon() else it.icon()),
contentDescription = it.label(),
modifier = Modifier.size(24.dp),
colorFilter = if (!isSelected) ColorFilter.tint(AppColors.text) else null
)
}
// 文字标签,可控制间距
Spacer(modifier = Modifier.height(4.dp))
Text(
text = it.label(),
fontSize = 10.sp,
color = if (isSelected) AppColors.brandColorsColor else AppColors.text,
fontWeight = if (isSelected) FontWeight.W600 else FontWeight.Normal
) )
} }
} }
}, },
label = { label = {
// Add按钮不显示文字标签 // 不显示默认标签
if (it.route != NavigationItem.Add.route) {
Text(
modifier = Modifier.padding(0.dp),
text = it.label(),
fontSize = 9.sp,
color = if (isSelected) AppColors.brandColorsColor else AppColors.text,
)
}
} }
) )
} }
} }
} }
@@ -344,7 +353,7 @@ fun IndexScreen() {
modifier = Modifier modifier = Modifier
.background(AppColors.background) .background(AppColors.background)
.padding(0.dp), .padding(0.dp),
beyondBoundsPageCount = 5, beyondBoundsPageCount = 4,
userScrollEnabled = false userScrollEnabled = false
) { page -> ) { page ->
when (page) { when (page) {

View File

@@ -3,57 +3,46 @@ package com.aiosman.ravenow.ui.index
import android.graphics.Color import android.graphics.Color
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.res.vectorResource import androidx.compose.ui.res.vectorResource
import com.aiosman.ravenow.R import com.aiosman.ravenow.R
sealed class NavigationItem( sealed class NavigationItem(
val route: String, val route: String,
val icon: @Composable () -> ImageVector, val icon: @Composable () -> Int,
val selectedIcon: @Composable () -> ImageVector = icon, val selectedIcon: @Composable () -> Int = icon,
val label: @Composable () -> String val label: @Composable () -> String
) { ) {
data object Home : NavigationItem("Home", data object Home : NavigationItem("Home",
icon = { ImageVector.vectorResource(R.drawable.rider_pro_nav_home) }, icon = { R.drawable.rider_pro_nav_home },
selectedIcon = { ImageVector.vectorResource(R.drawable.rider_pro_nav_home_hl) }, selectedIcon = { R.mipmap.rider_pro_nav_home_hl },
label = { stringResource(R.string.main_home) } label = { stringResource(R.string.main_home) }
) )
data object Street : NavigationItem("Street", data object Ai : NavigationItem("Ai",
icon = { ImageVector.vectorResource(R.drawable.rider_pro_location) }, icon = { R.drawable.rider_pro_nav_ai },
selectedIcon = { ImageVector.vectorResource(R.drawable.rider_pro_location_filed) }, selectedIcon = { R.mipmap.rider_pro_nav_ai_hl },
label = { stringResource(R.string.main_home) } label = { stringResource(R.string.main_home) }
) )
data object Add : NavigationItem("Add", data object Add : NavigationItem("Add",
icon = { ImageVector.vectorResource(R.drawable.ic_nav_add) }, icon = { R.drawable.ic_nav_add },
selectedIcon = { ImageVector.vectorResource(R.drawable.ic_nav_add) }, selectedIcon = { R.drawable.ic_nav_add },
label = { "" }
)
data object Message : NavigationItem("Message",
icon = { ImageVector.vectorResource(R.drawable.rider_pro_video_outline) },
selectedIcon = { ImageVector.vectorResource(R.drawable.rider_pro_video) },
label = { stringResource(R.string.main_home) } label = { stringResource(R.string.main_home) }
) )
data object Notification : NavigationItem("Notification", data object Notification : NavigationItem("Notification",
icon = { ImageVector.vectorResource(R.drawable.rider_pro_nav_notification)}, icon = { R.drawable.rider_pro_nav_notification },
selectedIcon = { ImageVector.vectorResource(R.drawable.rider_pro_nav_notification) }, selectedIcon = { R.mipmap.rider_pro_nav_message_hl },
label = { stringResource(R.string.main_message) } label = { stringResource(R.string.main_message) }
) )
data object Profile : NavigationItem("Profile", data object Profile : NavigationItem("Profile",
icon = { ImageVector.vectorResource(R.drawable.rider_pro_nav_profile) }, icon = { R.drawable.rider_pro_nav_profile },
selectedIcon = { ImageVector.vectorResource(R.drawable.rider_pro_nav_profile_hl) }, selectedIcon = { R.mipmap.rider_pro_nav_profile_hl },
label = { stringResource(R.string.main_profile) } label = { stringResource(R.string.main_profile) }
) )
data object Ai : NavigationItem("Ai",
icon = { ImageVector.vectorResource(R.drawable.rider_pro_nav_search) },
selectedIcon = { ImageVector.vectorResource(R.drawable.rider_pro_nav_search_hl) },
label = { stringResource(R.string.main_ai) }
)
} }

View File

@@ -80,7 +80,7 @@ fun NotificationsScreen() {
val navController = LocalNavController.current val navController = LocalNavController.current
val systemUiController = rememberSystemUiController() val systemUiController = rememberSystemUiController()
val context = LocalContext.current val context = LocalContext.current
var pagerState = rememberPagerState (pageCount = { 4 }) var pagerState = rememberPagerState (pageCount = { 3 })
var scope = rememberCoroutineScope() var scope = rememberCoroutineScope()
val state = rememberPullRefreshState(MessageListViewModel.isLoading, onRefresh = { val state = rememberPullRefreshState(MessageListViewModel.isLoading, onRefresh = {
MessageListViewModel.viewModelScope.launch { MessageListViewModel.viewModelScope.launch {
@@ -143,7 +143,7 @@ fun NotificationsScreen() {
modifier = Modifier modifier = Modifier
.size(24.dp) .size(24.dp)
.noRippleClickable { .noRippleClickable {
navController.navigate(NavigationRoute.CreateGroupChat.route)
}, },
colorFilter = ColorFilter.tint(AppColors.text) colorFilter = ColorFilter.tint(AppColors.text)
) )

View File

@@ -1,6 +1,7 @@
package com.aiosman.ravenow.ui.index.tabs.moment package com.aiosman.ravenow.ui.index.tabs.moment
import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.Image
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
@@ -31,6 +32,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.res.vectorResource import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
@@ -95,13 +97,16 @@ fun MomentsList() {
fontWeight = FontWeight.W600) fontWeight = FontWeight.W600)
Spacer(modifier = Modifier.height(4.dp)) Spacer(modifier = Modifier.height(4.dp))
Box( Image(
modifier = Modifier painter = painterResource(
.width(34.dp) if (pagerState.currentPage == 0) R.mipmap.tab_indicator_selected
.height(4.dp) else R.drawable.tab_indicator_unselected
.clip(RoundedCornerShape(16.dp)) ),
.background(if (pagerState.currentPage == 0) AppColors.brandColorsColor else AppColors.background) contentDescription = "tab indicator",
) modifier = Modifier
.width(34.dp)
.height(4.dp)
)
} }
Spacer(modifier = Modifier.width(16.dp)) Spacer(modifier = Modifier.width(16.dp))
@@ -122,13 +127,16 @@ fun MomentsList() {
fontWeight = FontWeight.W600) fontWeight = FontWeight.W600)
Spacer(modifier = Modifier.height(4.dp)) Spacer(modifier = Modifier.height(4.dp))
Box( Image(
modifier = Modifier painter = painterResource(
.width(34.dp) if (pagerState.currentPage == 1) R.mipmap.tab_indicator_selected
.height(4.dp) else R.drawable.tab_indicator_unselected
.clip(RoundedCornerShape(16.dp)) ),
.background(if (pagerState.currentPage == 1) AppColors.brandColorsColor else AppColors.background) contentDescription = "tab indicator",
) modifier = Modifier
.width(34.dp)
.height(4.dp)
)
} }
//热门tab //热门tab
@@ -150,12 +158,15 @@ fun MomentsList() {
fontWeight = FontWeight.W600) fontWeight = FontWeight.W600)
Spacer(modifier = Modifier.height(4.dp)) Spacer(modifier = Modifier.height(4.dp))
Box( Image(
painter = painterResource(
if (pagerState.currentPage == 2) R.mipmap.tab_indicator_selected
else R.drawable.tab_indicator_unselected
),
contentDescription = "tab indicator",
modifier = Modifier modifier = Modifier
.width(34.dp) .width(34.dp)
.height(4.dp) .height(4.dp)
.clip(RoundedCornerShape(16.dp))
.background(if (pagerState.currentPage == 2) AppColors.brandColorsColor else AppColors.background)
) )
} }

View File

@@ -0,0 +1,35 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<group
android:translateX="-351"
android:translateY="-797">
<group
android:translateY="791">
<group
android:translateX="351"
android:translateY="6">
<path
android:fillType="evenOdd"
android:strokeWidth="1"
android:pathData="M 0 0 L 24 0 L 24 24 L 0 24 Z" />
<path
android:fillType="evenOdd"
android:strokeColor="#000000"
android:strokeWidth="1.93476923"
android:strokeLineJoin="round"
android:strokeLineCap="round"
android:pathData="M 12 3 C 14.2091389993 3 16 4.79086100068 16 7 C 16 9.20913899932 14.2091389993 11 12 11 C 9.79086100068 11 8 9.20913899932 8 7 C 8 4.79086100068 9.79086100068 3 12 3 Z" />
<path
android:fillType="evenOdd"
android:strokeColor="#000000"
android:strokeWidth="1.93476923"
android:pathData="M12,14 C16.418278,14 20,16.0147186 20,18.5 C20,19.3079805 19.6214332,20.0662253 18.9586285,20.7216744 C18.5467837,20.900999 18.0920303,21 17.6141538,21 L6.38584615,21 C5.90796974,21 5.45321625,20.900999 5.04097677,20.7223879 C4.37856678,20.0662253 4,19.3079805 4,18.5 C4,16.0147186 7.581722,14 12,14 Z" />
</group>
</group>
</group>
</vector>

View File

@@ -1,20 +1,35 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android" <vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp" android:width="24dp"
android:height="24dp" android:height="24dp"
android:viewportWidth="24" android:viewportWidth="24"
android:viewportHeight="24"> android:viewportHeight="24">
<path
android:pathData="M12,9m-6,0a6,6 0,1 1,12 0a6,6 0,1 1,-12 0" <group
android:strokeWidth="1.93476923" android:translateX="-351"
android:fillColor="#00000000" android:translateY="-797">
android:strokeColor="#FFFFFF" <group
android:fillType="evenOdd"/> android:translateY="791">
<path <group
android:pathData="M2.906,20.25C4.782,17.001 8.248,14.999 12,14.999C15.752,14.999 19.218,17.001 21.094,20.25" android:translateX="351"
android:strokeLineJoin="round" android:translateY="6">
android:strokeWidth="1.93476923" <path
android:fillColor="#00000000" android:fillType="evenOdd"
android:strokeColor="#FFFFFF" android:strokeWidth="1"
android:fillType="evenOdd" android:pathData="M 0 0 L 24 0 L 24 24 L 0 24 Z" />
android:strokeLineCap="round"/> <path
android:fillType="evenOdd"
android:strokeColor="#000000"
android:strokeWidth="1.93476923"
android:strokeLineJoin="round"
android:strokeLineCap="round"
android:pathData="M 12 3 C 14.2091389993 3 16 4.79086100068 16 7 C 16 9.20913899932 14.2091389993 11 12 11 C 9.79086100068 11 8 9.20913899932 8 7 C 8 4.79086100068 9.79086100068 3 12 3 Z" />
<path
android:fillType="evenOdd"
android:strokeColor="#000000"
android:strokeWidth="1.93476923"
android:pathData="M12,14 C16.418278,14 20,16.0147186 20,18.5 C20,19.3079805 19.6214332,20.0662253 18.9586285,20.7216744 C18.5467837,20.900999 18.0920303,21 17.6141538,21 L6.38584615,21 C5.90796974,21 5.45321625,20.900999 5.04097677,20.7223879 C4.37856678,20.0662253 4,19.3079805 4,18.5 C4,16.0147186 7.581722,14 12,14 Z" />
</group>
</group>
</group>
</vector> </vector>

View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="34dp"
android:height="4dp"
android:viewportWidth="34"
android:viewportHeight="4">
<path
android:fillColor="#00000000"
android:pathData="M0,2C0,0.895 0.895,0 2,0L32,0C33.105,0 34,0.895 34,2C34,3.105 33.105,4 32,4L2,4C0.895,4 0,3.105 0,2Z"/>
</vector>

Binary file not shown.

After

Width:  |  Height:  |  Size: 650 B

View File

@@ -162,5 +162,9 @@
<string name="friend_chat_empty_subtitle">开始与朋友对话吧</string> <string name="friend_chat_empty_subtitle">开始与朋友对话吧</string>
<string name="friend_chat_me_prefix">我: </string> <string name="friend_chat_me_prefix">我: </string>
<string name="friend_chat_load_failed">加载失败</string> <string name="friend_chat_load_failed">加载失败</string>
<string name="create_group_chat">创建群聊</string>
<string name="quick_create">一键创建</string>
<string name="group_name">群聊名称</string>
<string name="search_placeholder">搜索</string>
</resources> </resources>

View File

@@ -158,5 +158,9 @@
<string name="friend_chat_empty_subtitle">开始与朋友对话吧</string> <string name="friend_chat_empty_subtitle">开始与朋友对话吧</string>
<string name="friend_chat_me_prefix">我: </string> <string name="friend_chat_me_prefix">我: </string>
<string name="friend_chat_load_failed">加载失败</string> <string name="friend_chat_load_failed">加载失败</string>
<string name="create_group_chat">Create Group Chat</string>
<string name="quick_create">Quick Create</string>
<string name="group_name">Group Name</string>
<string name="search_placeholder">Search</string>
</resources> </resources>