From 2d518cbd68b70fd1fa7e4df7b6d933f0a0f05096 Mon Sep 17 00:00:00 2001 From: weber Date: Tue, 12 Aug 2025 19:06:56 +0800 Subject: [PATCH] =?UTF-8?q?=E8=87=AA=E5=AE=9A=E4=B9=89NavigationItem?= =?UTF-8?q?=EF=BC=8C=E6=96=B0=E5=A2=9E=E7=BE=A4=E7=BB=84=E5=88=9B=E5=BB=BA?= =?UTF-8?q?=E9=A1=B5=E9=9D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/java/com/aiosman/ravenow/Colors.kt | 3 + .../main/java/com/aiosman/ravenow/ui/Navi.kt | 14 + .../com/aiosman/ravenow/ui/account/edit2.kt | 11 +- .../ravenow/ui/group/AiAgentListScreen.kt | 70 +++ .../ravenow/ui/group/CreateGroupChatScreen.kt | 397 ++++++++++++++++++ .../ui/group/CreateGroupChatViewModel.kt | 133 ++++++ .../ravenow/ui/group/FriendListScreen.kt | 70 +++ .../aiosman/ravenow/ui/group/MemberItem.kt | 65 +++ .../com/aiosman/ravenow/ui/index/Index.kt | 95 +++-- .../ravenow/ui/index/NavigationItem.kt | 39 +- .../ui/index/tabs/message/MessageList.kt | 4 +- .../ravenow/ui/index/tabs/moment/Moment.kt | 45 +- .../res/drawable/rider_pro_nav_message.xml | 35 ++ .../res/drawable/rider_pro_nav_profile.xml | 45 +- .../res/drawable/tab_indicator_unselected.xml | 11 + .../mipmap-xhdpi/tab_indicator_selected.png | Bin 0 -> 650 bytes app/src/main/res/values-zh/strings.xml | 4 + app/src/main/res/values/strings.xml | 4 + 18 files changed, 941 insertions(+), 104 deletions(-) create mode 100644 app/src/main/java/com/aiosman/ravenow/ui/group/AiAgentListScreen.kt create mode 100644 app/src/main/java/com/aiosman/ravenow/ui/group/CreateGroupChatScreen.kt create mode 100644 app/src/main/java/com/aiosman/ravenow/ui/group/CreateGroupChatViewModel.kt create mode 100644 app/src/main/java/com/aiosman/ravenow/ui/group/FriendListScreen.kt create mode 100644 app/src/main/java/com/aiosman/ravenow/ui/group/MemberItem.kt create mode 100644 app/src/main/res/drawable/rider_pro_nav_message.xml create mode 100644 app/src/main/res/drawable/tab_indicator_unselected.xml create mode 100644 app/src/main/res/mipmap-xhdpi/tab_indicator_selected.png diff --git a/app/src/main/java/com/aiosman/ravenow/Colors.kt b/app/src/main/java/com/aiosman/ravenow/Colors.kt index 0391df4..227fe80 100644 --- a/app/src/main/java/com/aiosman/ravenow/Colors.kt +++ b/app/src/main/java/com/aiosman/ravenow/Colors.kt @@ -17,6 +17,7 @@ open class AppThemeData( var loadingText: Color, var disabledBackground: Color, var background: Color, + var secondaryBackground: Color, var decentBackground: Color, var divider: Color, var inputBackground: Color, @@ -46,6 +47,7 @@ class LightThemeColors : AppThemeData( loadingText = Color(0xffffffff), disabledBackground = Color(0xFFD0D0D0), background = Color(0xFFFFFFFF), + secondaryBackground = Color(0xFFF7f7f7), divider = Color(0xFFEbEbEb), inputBackground = Color(0xFFF7f7f7), inputBackground2 = Color(0xFFFFFFFF), @@ -76,6 +78,7 @@ class DarkThemeColors : AppThemeData( loadingText = Color(0xff000000), disabledBackground = Color(0xFF3A3A3A), background = Color(0xFF121212), + secondaryBackground = Color(0xFF1C1C1C), divider = Color(0xFF282828), inputBackground = Color(0xFF1C1C1C), inputBackground2 = Color(0xFF1C1C1C), diff --git a/app/src/main/java/com/aiosman/ravenow/ui/Navi.kt b/app/src/main/java/com/aiosman/ravenow/ui/Navi.kt index 8cd3df0..1833cf1 100644 --- a/app/src/main/java/com/aiosman/ravenow/ui/Navi.kt +++ b/app/src/main/java/com/aiosman/ravenow/ui/Navi.kt @@ -32,6 +32,7 @@ import com.aiosman.ravenow.ui.account.AccountEditScreen2 import com.aiosman.ravenow.ui.account.AccountSetting import com.aiosman.ravenow.ui.account.ResetPasswordScreen 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.ChatScreen import com.aiosman.ravenow.ui.comment.CommentsScreen @@ -98,6 +99,7 @@ sealed class NavigationRoute( data object AccountSetting : NavigationRoute("AccountSetting") data object AboutScreen : NavigationRoute("AboutScreen") data object AddAgent : NavigationRoute("AddAgent") + data object CreateGroupChat : NavigationRoute("CreateGroupChat") } @@ -423,6 +425,18 @@ fun NavigationController( ) { AddAgentScreen() } + + composable( + route = NavigationRoute.CreateGroupChat.route, + enterTransition = { + fadeIn(animationSpec = tween(durationMillis = 0)) + }, + exitTransition = { + fadeOut(animationSpec = tween(durationMillis = 0)) + } + ) { + CreateGroupChatScreen() + } } diff --git a/app/src/main/java/com/aiosman/ravenow/ui/account/edit2.kt b/app/src/main/java/com/aiosman/ravenow/ui/account/edit2.kt index bcfacb6..6a52ba7 100644 --- a/app/src/main/java/com/aiosman/ravenow/ui/account/edit2.kt +++ b/app/src/main/java/com/aiosman/ravenow/ui/account/edit2.kt @@ -30,12 +30,14 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.lifecycle.viewModelScope +import com.aiosman.ravenow.AppState import com.aiosman.ravenow.LocalAppTheme import com.aiosman.ravenow.LocalNavController import com.aiosman.ravenow.R import com.aiosman.ravenow.ui.NavigationRoute import com.aiosman.ravenow.ui.comment.NoticeScreenHeader 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.form.FormTextInput 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( modifier = Modifier .fillMaxSize() .background(color = appColors.background), horizontalAlignment = Alignment.CenterHorizontally ) { - StatusBarSpacer() + //StatusBarSpacer() Box( modifier = Modifier.padding(horizontal = 24.dp, vertical = 16.dp) ) { @@ -184,7 +191,7 @@ fun AccountEditScreen2() { } } - } + }} } \ No newline at end of file diff --git a/app/src/main/java/com/aiosman/ravenow/ui/group/AiAgentListScreen.kt b/app/src/main/java/com/aiosman/ravenow/ui/group/AiAgentListScreen.kt new file mode 100644 index 0000000..94f2fce --- /dev/null +++ b/app/src/main/java/com/aiosman/ravenow/ui/group/AiAgentListScreen.kt @@ -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 = emptySet(), + onMemberSelect: (GroupMember) -> Unit +) { + val AppColors = LocalAppTheme.current + val context = LocalContext.current + + var aiAgents by remember { mutableStateOf>(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) } + ) + } + } + } +} diff --git a/app/src/main/java/com/aiosman/ravenow/ui/group/CreateGroupChatScreen.kt b/app/src/main/java/com/aiosman/ravenow/ui/group/CreateGroupChatScreen.kt new file mode 100644 index 0000000..bd138b9 --- /dev/null +++ b/app/src/main/java/com/aiosman/ravenow/ui/group/CreateGroupChatScreen.kt @@ -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()) } + var selectedMemberIds by remember { mutableStateOf>(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 + ) + } + } + } +} + + diff --git a/app/src/main/java/com/aiosman/ravenow/ui/group/CreateGroupChatViewModel.kt b/app/src/main/java/com/aiosman/ravenow/ui/group/CreateGroupChatViewModel.kt new file mode 100644 index 0000000..f2767a7 --- /dev/null +++ b/app/src/main/java/com/aiosman/ravenow/ui/group/CreateGroupChatViewModel.kt @@ -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(null) + + // 获取AI智能体列表 + suspend fun getAiAgents(): List { + 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 { + 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, + 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, selectedMembers: List): Pair, List> { + 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, selectedMembers: List): Pair, List> { + val newSelectedMemberIds = selectedMemberIds - member.id + val newSelectedMembers = selectedMembers.filter { it.id != member.id } + return Pair(newSelectedMemberIds, newSelectedMembers) + } + + // 切换成员选中状态 + fun toggleMemberSelection(member: GroupMember, selectedMemberIds: Set, selectedMembers: List): Pair, List> { + return if (selectedMemberIds.contains(member.id)) { + removeSelectedMember(member, selectedMemberIds, selectedMembers) + } else { + addSelectedMember(member, selectedMemberIds, selectedMembers) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/aiosman/ravenow/ui/group/FriendListScreen.kt b/app/src/main/java/com/aiosman/ravenow/ui/group/FriendListScreen.kt new file mode 100644 index 0000000..ded192a --- /dev/null +++ b/app/src/main/java/com/aiosman/ravenow/ui/group/FriendListScreen.kt @@ -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 = emptySet(), + onMemberSelect: (GroupMember) -> Unit +) { + val AppColors = LocalAppTheme.current + val context = LocalContext.current + + var friends by remember { mutableStateOf>(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) } + ) + } + } + } +} diff --git a/app/src/main/java/com/aiosman/ravenow/ui/group/MemberItem.kt b/app/src/main/java/com/aiosman/ravenow/ui/group/MemberItem.kt new file mode 100644 index 0000000..b73894d --- /dev/null +++ b/app/src/main/java/com/aiosman/ravenow/ui/group/MemberItem.kt @@ -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 + ) + ) + } +} diff --git a/app/src/main/java/com/aiosman/ravenow/ui/index/Index.kt b/app/src/main/java/com/aiosman/ravenow/ui/index/Index.kt index d917146..d9272cd 100644 --- a/app/src/main/java/com/aiosman/ravenow/ui/index/Index.kt +++ b/app/src/main/java/com/aiosman/ravenow/ui/index/Index.kt @@ -52,6 +52,7 @@ import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp @@ -259,7 +260,7 @@ fun IndexScreen() { Scaffold( bottomBar = { NavigationBar( - modifier = Modifier.height(56.dp + navigationBarHeight), + modifier = Modifier.height(72.dp + navigationBarHeight), containerColor = AppColors.background ) { item.forEachIndexed { idx, it -> @@ -268,6 +269,7 @@ fun IndexScreen() { targetValue = if (isSelected) AppColors.brandColorsColor else AppColors.text, animationSpec = tween(durationMillis = 250), label = "" ) + NavigationBarItem( modifier = Modifier.padding(top = 6.dp), selected = isSelected, @@ -282,58 +284,65 @@ fun IndexScreen() { } model.tabIndex = idx }, - interactionSource = remember { MutableInteractionSource() }, colors = NavigationBarItemColors( - selectedTextColor = Color.Red, + selectedIconColor = Color.Transparent, + selectedTextColor = Color.Transparent, selectedIndicatorColor = Color.Transparent, - unselectedTextColor = Color.Red, - disabledIconColor = Color.Red, - disabledTextColor = Color.Red, - selectedIconColor = iconTint, - unselectedIconColor = iconTint, + unselectedIconColor = Color.Transparent, + unselectedTextColor = Color.Transparent, + disabledIconColor = Color.Transparent, + disabledTextColor = Color.Transparent ), icon = { - // 特殊处理中间的Add按钮,只显示图标并放大 - if (it.route == NavigationItem.Add.route) { - Icon( - modifier = Modifier.size(32.dp), - imageVector = if (isSelected) it.selectedIcon() else it.icon(), - contentDescription = null, - tint = AppColors.text - ) - } else { - Box( - modifier = Modifier - .width(46.dp) - .height(30.dp) - .background( - color = if (isSelected) AppColors.brandColorsColor.copy(alpha = 0.1f) else Color.Transparent , - shape = RoundedCornerShape(10.dp) - ), - contentAlignment = Alignment.Center - ) { - Icon( - modifier = Modifier.size(24.dp), - imageVector = if (isSelected) it.selectedIcon() else it.icon(), - contentDescription = null, - tint = iconTint + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + if (it.route == NavigationItem.Add.route) { + // Add按钮:只显示大图标 + Image( + painter = painterResource(if (isSelected) it.selectedIcon() else it.icon()), + contentDescription = it.label(), + modifier = Modifier.size(32.dp), + colorFilter = if (!isSelected) ColorFilter.tint(AppColors.text) else null + ) + } else { + // 其他按钮:图标+文字 + Box( + modifier = Modifier + .width(48.dp) + .height(32.dp) + .background( + color = if (isSelected) AppColors.brandColorsColor.copy(alpha = 0.15f) else Color.Transparent, + shape = RoundedCornerShape(12.dp) + ), + contentAlignment = Alignment.Center + ) { + 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 = { - // 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 .background(AppColors.background) .padding(0.dp), - beyondBoundsPageCount = 5, + beyondBoundsPageCount = 4, userScrollEnabled = false ) { page -> when (page) { diff --git a/app/src/main/java/com/aiosman/ravenow/ui/index/NavigationItem.kt b/app/src/main/java/com/aiosman/ravenow/ui/index/NavigationItem.kt index 1fde340..e27d24a 100644 --- a/app/src/main/java/com/aiosman/ravenow/ui/index/NavigationItem.kt +++ b/app/src/main/java/com/aiosman/ravenow/ui/index/NavigationItem.kt @@ -3,57 +3,46 @@ package com.aiosman.ravenow.ui.index import android.graphics.Color import androidx.compose.runtime.Composable import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.vectorResource import com.aiosman.ravenow.R sealed class NavigationItem( val route: String, - val icon: @Composable () -> ImageVector, - val selectedIcon: @Composable () -> ImageVector = icon, + val icon: @Composable () -> Int, + val selectedIcon: @Composable () -> Int = icon, val label: @Composable () -> String ) { data object Home : NavigationItem("Home", - icon = { ImageVector.vectorResource(R.drawable.rider_pro_nav_home) }, - selectedIcon = { ImageVector.vectorResource(R.drawable.rider_pro_nav_home_hl) }, + icon = { R.drawable.rider_pro_nav_home }, + selectedIcon = { R.mipmap.rider_pro_nav_home_hl }, label = { stringResource(R.string.main_home) } ) - data object Street : NavigationItem("Street", - icon = { ImageVector.vectorResource(R.drawable.rider_pro_location) }, - selectedIcon = { ImageVector.vectorResource(R.drawable.rider_pro_location_filed) }, + data object Ai : NavigationItem("Ai", + icon = { R.drawable.rider_pro_nav_ai }, + selectedIcon = { R.mipmap.rider_pro_nav_ai_hl }, label = { stringResource(R.string.main_home) } - ) data object Add : NavigationItem("Add", - icon = { ImageVector.vectorResource(R.drawable.ic_nav_add) }, - selectedIcon = { ImageVector.vectorResource(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) }, + icon = { R.drawable.ic_nav_add }, + selectedIcon = { R.drawable.ic_nav_add }, label = { stringResource(R.string.main_home) } ) data object Notification : NavigationItem("Notification", - icon = { ImageVector.vectorResource(R.drawable.rider_pro_nav_notification)}, - selectedIcon = { ImageVector.vectorResource(R.drawable.rider_pro_nav_notification) }, + icon = { R.drawable.rider_pro_nav_notification }, + selectedIcon = { R.mipmap.rider_pro_nav_message_hl }, label = { stringResource(R.string.main_message) } ) data object Profile : NavigationItem("Profile", - icon = { ImageVector.vectorResource(R.drawable.rider_pro_nav_profile) }, - selectedIcon = { ImageVector.vectorResource(R.drawable.rider_pro_nav_profile_hl) }, + icon = { R.drawable.rider_pro_nav_profile }, + selectedIcon = { R.mipmap.rider_pro_nav_profile_hl }, 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) } - ) } \ No newline at end of file diff --git a/app/src/main/java/com/aiosman/ravenow/ui/index/tabs/message/MessageList.kt b/app/src/main/java/com/aiosman/ravenow/ui/index/tabs/message/MessageList.kt index f294898..e41484d 100644 --- a/app/src/main/java/com/aiosman/ravenow/ui/index/tabs/message/MessageList.kt +++ b/app/src/main/java/com/aiosman/ravenow/ui/index/tabs/message/MessageList.kt @@ -80,7 +80,7 @@ fun NotificationsScreen() { val navController = LocalNavController.current val systemUiController = rememberSystemUiController() val context = LocalContext.current - var pagerState = rememberPagerState (pageCount = { 4 }) + var pagerState = rememberPagerState (pageCount = { 3 }) var scope = rememberCoroutineScope() val state = rememberPullRefreshState(MessageListViewModel.isLoading, onRefresh = { MessageListViewModel.viewModelScope.launch { @@ -143,7 +143,7 @@ fun NotificationsScreen() { modifier = Modifier .size(24.dp) .noRippleClickable { - + navController.navigate(NavigationRoute.CreateGroupChat.route) }, colorFilter = ColorFilter.tint(AppColors.text) ) diff --git a/app/src/main/java/com/aiosman/ravenow/ui/index/tabs/moment/Moment.kt b/app/src/main/java/com/aiosman/ravenow/ui/index/tabs/moment/Moment.kt index f0e3bb7..d9ac943 100644 --- a/app/src/main/java/com/aiosman/ravenow/ui/index/tabs/moment/Moment.kt +++ b/app/src/main/java/com/aiosman/ravenow/ui/index/tabs/moment/Moment.kt @@ -1,6 +1,7 @@ package com.aiosman.ravenow.ui.index.tabs.moment import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -31,6 +32,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.vectorResource import androidx.compose.ui.text.font.FontWeight @@ -95,13 +97,16 @@ fun MomentsList() { fontWeight = FontWeight.W600) Spacer(modifier = Modifier.height(4.dp)) - Box( - modifier = Modifier - .width(34.dp) - .height(4.dp) - .clip(RoundedCornerShape(16.dp)) - .background(if (pagerState.currentPage == 0) AppColors.brandColorsColor else AppColors.background) - ) + Image( + painter = painterResource( + if (pagerState.currentPage == 0) R.mipmap.tab_indicator_selected + else R.drawable.tab_indicator_unselected + ), + contentDescription = "tab indicator", + modifier = Modifier + .width(34.dp) + .height(4.dp) + ) } Spacer(modifier = Modifier.width(16.dp)) @@ -122,13 +127,16 @@ fun MomentsList() { fontWeight = FontWeight.W600) Spacer(modifier = Modifier.height(4.dp)) - Box( - modifier = Modifier - .width(34.dp) - .height(4.dp) - .clip(RoundedCornerShape(16.dp)) - .background(if (pagerState.currentPage == 1) AppColors.brandColorsColor else AppColors.background) - ) + Image( + painter = painterResource( + if (pagerState.currentPage == 1) R.mipmap.tab_indicator_selected + else R.drawable.tab_indicator_unselected + ), + contentDescription = "tab indicator", + modifier = Modifier + .width(34.dp) + .height(4.dp) + ) } //热门tab @@ -150,12 +158,15 @@ fun MomentsList() { fontWeight = FontWeight.W600) 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 .width(34.dp) .height(4.dp) - .clip(RoundedCornerShape(16.dp)) - .background(if (pagerState.currentPage == 2) AppColors.brandColorsColor else AppColors.background) ) } diff --git a/app/src/main/res/drawable/rider_pro_nav_message.xml b/app/src/main/res/drawable/rider_pro_nav_message.xml new file mode 100644 index 0000000..cd57db4 --- /dev/null +++ b/app/src/main/res/drawable/rider_pro_nav_message.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/rider_pro_nav_profile.xml b/app/src/main/res/drawable/rider_pro_nav_profile.xml index a1eaf98..e16501e 100644 --- a/app/src/main/res/drawable/rider_pro_nav_profile.xml +++ b/app/src/main/res/drawable/rider_pro_nav_profile.xml @@ -1,20 +1,35 @@ + - - - + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/tab_indicator_unselected.xml b/app/src/main/res/drawable/tab_indicator_unselected.xml new file mode 100644 index 0000000..58a7927 --- /dev/null +++ b/app/src/main/res/drawable/tab_indicator_unselected.xml @@ -0,0 +1,11 @@ + + + + + diff --git a/app/src/main/res/mipmap-xhdpi/tab_indicator_selected.png b/app/src/main/res/mipmap-xhdpi/tab_indicator_selected.png new file mode 100644 index 0000000000000000000000000000000000000000..3028065d48a5be28fa487efe73ab227fd14d4be3 GIT binary patch literal 650 zcmV;50(Jd~P)Px%L`g(JR7gvWmCtS6Fbu{?8fb3?vP6)adI^dQkO{g04L5KeL@zZSCRen(q(n3p)Rc=9zmj(8tOod0Vh0@io5ZiGL1;jiC* zeEEgz&ByBv-aT`}D@mNSa1-EIOp5bgbMY+~uz`q20PLb*6iKB}FHn;8BBkJMc&z5Cmmw250gcCWUH zw2tp?2zS;YB4GcBWb`0@?D;tD4$5xw(vuB9ye7a%Bh|UD-_X9&F4u~~p9TFT02L)S07*qoM6N<$g4;MGdH?_b literal 0 HcmV?d00001 diff --git a/app/src/main/res/values-zh/strings.xml b/app/src/main/res/values-zh/strings.xml index 423b50c..8cbca05 100644 --- a/app/src/main/res/values-zh/strings.xml +++ b/app/src/main/res/values-zh/strings.xml @@ -162,5 +162,9 @@ 开始与朋友对话吧 我: 加载失败 + 创建群聊 + 一键创建 + 群聊名称 + 搜索 \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 7b83809..b99f5ae 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -158,5 +158,9 @@ 开始与朋友对话吧 我: 加载失败 + Create Group Chat + Quick Create + Group Name + Search \ No newline at end of file