From 8a76bd11d92d781d13ca40cbc3d096354f296fde Mon Sep 17 00:00:00 2001 From: AllenTom Date: Wed, 19 Nov 2025 23:51:43 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=A2=9E=E5=8A=A0Google=E6=94=AF?= =?UTF-8?q?=E4=BB=98=E5=8F=8A=E4=BC=98=E5=8C=96UI=E4=B8=8E=E6=80=A7?= =?UTF-8?q?=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - **支付功能**: - 在应用启动时初始化Google Play Billing Client,为应用内购买做准备。 - 添加了`billing-ktx`依赖。 - **动态和个人主页**: - 动态推荐页:用户头像和昵称区域支持点击跳转到对应的个人资料页。 - 个人资料页:优化了用户资料加载逻辑,使其同时支持通过用户ID和OpenID加载。 - 评论功能:优化了评论交互,评论成功后才更新评论数。 - 数据模型:调整动态图片,优先使用`smallDirectUrl`以优化加载速度。 - **AI智能体页面**: - 移除API请求中的`random`参数,以改善数据缓存和一致性。 - 优化了导航到AI智能体主页的逻辑,直接传递`openId`,简化了数据请求。 - 清理了部分未使用的代码和布局。 - **群聊列表UI**: - 调整了群聊列表项的布局、字体大小和颜色,优化了视觉样式。 - 移除了列表项之间的分割线。 --- app/build.gradle.kts | 7 +- .../com/aiosman/ravenow/RaveNowApplication.kt | 53 +++ .../com/aiosman/ravenow/data/MomentService.kt | 16 +- .../aiosman/ravenow/ui/index/tabs/ai/Agent.kt | 448 ++++-------------- .../ui/index/tabs/ai/AgentViewModel.kt | 29 +- .../tabs/message/tab/GroupChatListScreen.kt | 37 +- .../tabs/recommend/PostRecommendationItem.kt | 95 ++-- .../moment/tabs/recommend/RecommendScreen.kt | 10 +- .../ravenow/ui/profile/AiProfileViewModel.kt | 19 +- gradle/libs.versions.toml | 2 + 10 files changed, 283 insertions(+), 433 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index d683e0a..65d2a6b 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -17,8 +17,8 @@ android { applicationId = "com.aiosman.ravenow" minSdk = 24 targetSdk = 35 - versionCode = 1000019 - versionName = "1.0.000.19" + versionCode = 1000021 + versionName = "1.0.000.21" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" vectorDrawables { @@ -140,5 +140,8 @@ dependencies { implementation(libs.androidx.room.ktx) ksp(libs.androidx.room.compiler) + // Google Play Billing + implementation(libs.billing.ktx) + } diff --git a/app/src/main/java/com/aiosman/ravenow/RaveNowApplication.kt b/app/src/main/java/com/aiosman/ravenow/RaveNowApplication.kt index 367187a..cd2011d 100644 --- a/app/src/main/java/com/aiosman/ravenow/RaveNowApplication.kt +++ b/app/src/main/java/com/aiosman/ravenow/RaveNowApplication.kt @@ -4,6 +4,10 @@ import android.app.Application import android.content.Context import android.content.res.Configuration import android.util.Log +import com.android.billingclient.api.BillingClient +import com.android.billingclient.api.BillingClientStateListener +import com.android.billingclient.api.BillingResult +import com.android.billingclient.api.PurchasesUpdatedListener import com.google.firebase.FirebaseApp import com.google.firebase.perf.FirebasePerformance @@ -12,6 +16,8 @@ import com.google.firebase.perf.FirebasePerformance */ class RaveNowApplication : Application() { + private var billingClient: BillingClient? = null + override fun attachBaseContext(base: Context) { // 禁用字体缩放,固定字体大小为系统默认大小 val configuration = Configuration(base.resources.configuration) @@ -48,6 +54,53 @@ class RaveNowApplication : Application() { } catch (e: Exception) { Log.e("RaveNowApplication", "Firebase初始化失败在进程 $processName", e) } + + // 初始化 Google Play Billing + initBillingClient() + } + + /** + * 初始化 Google Play Billing Client + */ + private fun initBillingClient() { + val purchasesUpdatedListener = PurchasesUpdatedListener { billingResult, purchases -> + if (billingResult.responseCode == BillingClient.BillingResponseCode.OK && purchases != null) { + // 处理购买成功 + Log.d("RaveNowApplication", "购买成功: ${purchases.size} 个商品") + } else if (billingResult.responseCode == BillingClient.BillingResponseCode.USER_CANCELED) { + // 用户取消购买 + Log.d("RaveNowApplication", "用户取消购买") + } else { + // 处理其他错误 + Log.e("RaveNowApplication", "购买失败: ${billingResult.debugMessage}") + } + } + + billingClient = BillingClient.newBuilder(this) + .setListener(purchasesUpdatedListener) + .build() + + billingClient?.startConnection(object : BillingClientStateListener { + override fun onBillingSetupFinished(billingResult: BillingResult) { + if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) { + Log.d("RaveNowApplication", "BillingClient 初始化成功") + } else { + Log.e("RaveNowApplication", "BillingClient 初始化失败: ${billingResult.debugMessage}") + } + } + + override fun onBillingServiceDisconnected() { + Log.w("RaveNowApplication", "BillingClient 连接断开,尝试重新连接") + // 可以在这里实现重连逻辑 + } + }) + } + + /** + * 获取 BillingClient 实例 + */ + fun getBillingClient(): BillingClient? { + return billingClient } /** diff --git a/app/src/main/java/com/aiosman/ravenow/data/MomentService.kt b/app/src/main/java/com/aiosman/ravenow/data/MomentService.kt index 11ff9d6..cf06c00 100644 --- a/app/src/main/java/com/aiosman/ravenow/data/MomentService.kt +++ b/app/src/main/java/com/aiosman/ravenow/data/MomentService.kt @@ -35,6 +35,10 @@ data class Moment( val commentCount: Long, @SerializedName("time") val time: String?, + @SerializedName("createdAt") + val createdAt: String? = null, + @SerializedName("location") + val location: String? = null, @SerializedName("isFollowed") val isFollowed: Boolean, // 新闻相关字段 @@ -70,11 +74,11 @@ data class Moment( "" // 如果头像为空,使用空字符串 }, nickname = user.nickName ?: "未知用户", // 如果昵称为空,使用默认值 - location = "Worldwide", - time = if (time != null && time.isNotEmpty()) { - ApiClient.dateFromApiString(time) - } else { - java.util.Date() // 如果时间为空,使用当前时间作为默认值 + location = location ?: "Worldwide", + time = when { + createdAt != null && createdAt.isNotEmpty() -> ApiClient.dateFromApiString(createdAt) + time != null && time.isNotEmpty() -> ApiClient.dateFromApiString(time) + else -> java.util.Date() // 如果时间为空,使用当前时间作为默认值 }, followStatus = isFollowed, momentTextContent = textContent, @@ -204,7 +208,7 @@ data class Video( data class User( @SerializedName("id") val id: Long, - @SerializedName("nickName") + @SerializedName("nickname") val nickName: String?, @SerializedName("avatar") val avatar: String?, diff --git a/app/src/main/java/com/aiosman/ravenow/ui/index/tabs/ai/Agent.kt b/app/src/main/java/com/aiosman/ravenow/ui/index/tabs/ai/Agent.kt index f0c1bb6..364089a 100644 --- a/app/src/main/java/com/aiosman/ravenow/ui/index/tabs/ai/Agent.kt +++ b/app/src/main/java/com/aiosman/ravenow/ui/index/tabs/ai/Agent.kt @@ -16,7 +16,6 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.navigationBars -import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.systemBars @@ -24,22 +23,18 @@ import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.foundation.pager.HorizontalPager -import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.Icon import androidx.compose.material.Text import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Scaffold import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable -import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -63,33 +58,24 @@ import com.aiosman.ravenow.LocalNavController import com.aiosman.ravenow.R import com.aiosman.ravenow.ui.NavigationRoute import com.aiosman.ravenow.ui.composables.CustomAsyncImage -import com.aiosman.ravenow.ui.index.tabs.ai.tabs.mine.MineAgent -import com.aiosman.ravenow.ui.index.tabs.ai.tabs.hot.HotAgent import com.aiosman.ravenow.ui.modifiers.noRippleClickable -import com.aiosman.ravenow.ui.composables.TabItem import com.aiosman.ravenow.ui.composables.TabSpacer import com.aiosman.ravenow.ui.index.tabs.moment.CustomTabItem import com.aiosman.ravenow.ui.index.tabs.moment.tabs.expolre.AgentItem -import com.aiosman.ravenow.ui.index.tabs.moment.tabs.expolre.ExploreViewModel import com.aiosman.ravenow.utils.DebounceUtils -import com.aiosman.ravenow.utils.ResourceCleanupManager -import kotlinx.coroutines.launch import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.foundation.lazy.grid.GridCells -import androidx.compose.foundation.lazy.grid.LazyVerticalGrid import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.DialogProperties -import androidx.compose.foundation.lazy.grid.items as gridItems -import androidx.compose.material.CircularProgressIndicator import androidx.compose.ui.draw.alpha import androidx.compose.ui.graphics.Brush import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.ui.platform.LocalConfiguration import com.airbnb.lottie.compose.LottieAnimation import com.airbnb.lottie.compose.LottieCompositionSpec @@ -113,10 +99,6 @@ fun Agent() { val navigationBarPaddings = WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() + 48.dp val statusBarPaddingValues = WindowInsets.systemBars.asPaddingValues() - // 游客模式下只显示热门Agent,正常用户显示我的Agent和热门Agent - val tabCount = if (AppStore.isGuest) 1 else 2 - var pagerState = rememberPagerState { tabCount } - var scope = rememberCoroutineScope() val viewModel: AgentViewModel = AgentViewModel @@ -125,16 +107,6 @@ fun Agent() { viewModel.ensureDataLoaded() } - // 防抖状态 - var lastClickTime by remember { mutableStateOf(0L) } - - // 页面退出时只清理必要的资源,不清理推荐Agent数据 - DisposableEffect(Unit) { - onDispose { - // 只清理子页面的资源,保留推荐Agent数据 - // ResourceCleanupManager.cleanupPageResources("ai") - } - } val agentItems = viewModel.agentItems var selectedTabIndex by remember { mutableStateOf(0) } @@ -165,7 +137,7 @@ fun Agent() { contentDescription = "Rave AI Logo", modifier = Modifier .height(44.dp) - .padding(top =9.dp,bottom=9.dp) + .padding(top = 9.dp, bottom = 9.dp) .wrapContentSize(), // colorFilter = ColorFilter.tint(AppColors.text) ) @@ -176,7 +148,7 @@ fun Agent() { contentDescription = "search", modifier = Modifier .size(44.dp) - .padding(top = 9.dp,bottom=9.dp) + .padding(top = 9.dp, bottom = 9.dp) .noRippleClickable { navController.navigate(NavigationRoute.Search.route) }, @@ -267,11 +239,19 @@ fun Agent() { ) { when { selectedTabIndex == 0 -> { - AgentViewPagerSection(agentItems = viewModel.agentItems.take(15), viewModel) + AgentViewPagerSection( + agentItems = viewModel.agentItems.take(15), + viewModel + ) } + selectedTabIndex in 1..viewModel.categories.size -> { - AgentViewPagerSection(agentItems = viewModel.agentItems.take(15), viewModel) + AgentViewPagerSection( + agentItems = viewModel.agentItems.take(15), + viewModel + ) } + else -> { val shuffledAgents = viewModel.agentItems.shuffled().take(15) AgentViewPagerSection(agentItems = shuffledAgents, viewModel) @@ -329,133 +309,81 @@ fun Agent() { } // 只有当热门聊天室有数据时,才展示“发现更多”区域 - if (viewModel.chatRooms.isNotEmpty()) { - item { Spacer(modifier = Modifier.height(20.dp)) } + item { Spacer(modifier = Modifier.height(20.dp)) } - // "发现更多" 标题 - 吸顶 - stickyHeader(key = "discover_more") { - Row( - modifier = Modifier - .fillMaxWidth() - .background(AppColors.background) - .padding(top = 8.dp, bottom = 12.dp), - horizontalArrangement = Arrangement.Start, - verticalAlignment = Alignment.Bottom - ) { - Image( - painter = painterResource(R.mipmap.bars_x_buttons_home_n_copy_2), - contentDescription = "agent", - modifier = Modifier.size(28.dp) - ) - Spacer(modifier = Modifier.width(4.dp)) - androidx.compose.material3.Text( - text = stringResource(R.string.agent_find), - fontSize = 16.sp, - fontWeight = androidx.compose.ui.text.font.FontWeight.W900, - color = AppColors.text - ) - } + // "发现更多" 标题 - 吸顶 + stickyHeader(key = "discover_more") { + Row( + modifier = Modifier + .fillMaxWidth() + .background(AppColors.background) + .padding(top = 8.dp, bottom = 12.dp), + horizontalArrangement = Arrangement.Start, + verticalAlignment = Alignment.Bottom + ) { + Image( + painter = painterResource(R.mipmap.bars_x_buttons_home_n_copy_2), + contentDescription = "agent", + modifier = Modifier.size(28.dp) + ) + Spacer(modifier = Modifier.width(4.dp)) + androidx.compose.material3.Text( + text = stringResource(R.string.agent_find), + fontSize = 16.sp, + fontWeight = androidx.compose.ui.text.font.FontWeight.W900, + color = AppColors.text + ) } + } - // Agent网格 - 使用行式布局 - items( - items = agentItems.chunked(2), - key = { row -> row.firstOrNull()?.openId ?: "" } - ) { rowItems -> - Row( - modifier = Modifier - .fillMaxWidth() - .padding(vertical = 16.dp), - horizontalArrangement = Arrangement.spacedBy(16.dp) - ) { - rowItems.forEach { agentItem -> - Box( - modifier = Modifier.weight(1f) - ) { - AgentCardSquare( - agentItem = agentItem, - viewModel = viewModel, - navController = LocalNavController.current - ) - } - } - // 如果这一行只有一个item,添加一个空的占位符 - if (rowItems.size == 1) { - Spacer(modifier = Modifier.weight(1f)) - } - } - } - - // 加载更多指示器(仅在展示"发现更多"时显示) - if (viewModel.isLoadingMore) { - item { + // Agent网格 - 使用行式布局 + items( + items = agentItems.chunked(2), + key = { row -> row.firstOrNull()?.openId ?: "" } + ) { rowItems -> + Row( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 16.dp), + horizontalArrangement = Arrangement.spacedBy(16.dp) + ) { + rowItems.forEach { agentItem -> Box( - modifier = Modifier - .fillMaxWidth() - .padding(vertical = 24.dp), - contentAlignment = Alignment.Center + modifier = Modifier.weight(1f) ) { - LottieAnimation( - composition = rememberLottieComposition(LottieCompositionSpec.Asset("star_Loader.lottie")).value, - iterations = LottieConstants.IterateForever, - modifier = Modifier.size(80.dp) + AgentCardSquare( + agentItem = agentItem, + viewModel = viewModel, + navController = LocalNavController.current ) } } + // 如果这一行只有一个item,添加一个空的占位符 + if (rowItems.size == 1) { + Spacer(modifier = Modifier.weight(1f)) + } } } - } - } -} -@Composable -fun AgentGridLayout( - agentItems: List, - viewModel: AgentViewModel, - navController: NavHostController -) { - Column( - modifier = Modifier.fillMaxWidth() - ) { - // 将agentItems按两列分组 - agentItems.chunked(2).forEachIndexed { rowIndex, rowItems -> - Row( - modifier = Modifier - .fillMaxWidth() - .padding( - top = if (rowIndex == 0) 30.dp else 20.dp, // 第一行添加更多顶部间距 - bottom = 20.dp - ), - horizontalArrangement = Arrangement.spacedBy(16.dp) - ) { - // 第一列 - Box( - modifier = Modifier.weight(1f) - ) { - AgentCardSquare( - agentItem = rowItems[0], - viewModel = viewModel, - navController = navController - ) - } - - // 第二列(如果存在) - if (rowItems.size > 1) { + // 加载更多指示器(仅在展示"发现更多"时显示) + if (viewModel.isLoadingMore) { + item { Box( - modifier = Modifier.weight(1f) + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 24.dp), + contentAlignment = Alignment.Center ) { - AgentCardSquare( - agentItem = rowItems[1], - viewModel = viewModel, - navController = navController + LottieAnimation( + composition = rememberLottieComposition(LottieCompositionSpec.Asset("star_Loader.lottie")).value, + iterations = LottieConstants.IterateForever, + modifier = Modifier.size(80.dp) ) } - } else { - // 如果只有一列,添加空白占位 - Spacer(modifier = Modifier.weight(1f)) } } } + } } @@ -486,11 +414,11 @@ fun AgentCardSquare( } ) { // 背景大图 - CustomAsyncImage( - imageUrl = agentItem.avatar, + CustomAsyncImage( + imageUrl = agentItem.avatar, contentDescription = agentItem.title, modifier = Modifier.fillMaxSize(), - contentScale = androidx.compose.ui.layout.ContentScale.Crop, + contentScale = androidx.compose.ui.layout.ContentScale.Crop, defaultRes = R.mipmap.rider_pro_agent ) @@ -507,27 +435,27 @@ fun AgentCardSquare( ) .padding(12.dp) ) { - Column( - modifier = Modifier - .fillMaxWidth() + Column( + modifier = Modifier + .fillMaxWidth() .padding(bottom = 40.dp) // 为底部聊天按钮预留空间 - ) { - androidx.compose.material3.Text( - text = agentItem.title, + ) { + androidx.compose.material3.Text( + text = agentItem.title, color = Color.White, - fontSize = 14.sp, + fontSize = 14.sp, fontWeight = androidx.compose.ui.text.font.FontWeight.W700, - maxLines = 1, + maxLines = 1, overflow = TextOverflow.Ellipsis - ) + ) Spacer(modifier = Modifier.height(4.dp)) - androidx.compose.material3.Text( - text = agentItem.desc, + androidx.compose.material3.Text( + text = agentItem.desc, color = Color.White.copy(alpha = 0.92f), fontSize = 11.sp, - maxLines = 2, + maxLines = 2, overflow = TextOverflow.Ellipsis - ) + ) } } @@ -562,9 +490,10 @@ fun AgentCardSquare( } } } + @OptIn(ExperimentalFoundationApi::class) @Composable -fun AgentViewPagerSection(agentItems: List,viewModel: AgentViewModel) { +fun AgentViewPagerSection(agentItems: List, viewModel: AgentViewModel) { val AppColors = LocalAppTheme.current if (agentItems.isEmpty()) return @@ -711,186 +640,6 @@ fun AgentLargeCard( } } -@Composable -fun AgentPage(viewModel: AgentViewModel,agentItems: List, page: Int, modifier: Modifier = Modifier,navController: NavHostController) { - Column( - modifier = modifier - .fillMaxSize() - .padding(horizontal = 0.dp) - ) { - // 显示3个agent - agentItems.forEachIndexed { index, agentItem -> - AgentCard2(agentItem = agentItem, viewModel = viewModel, navController = LocalNavController.current) - if (index < agentItems.size - 1) { - Spacer(modifier = Modifier.height(8.dp)) - } - } - } -} - -@SuppressLint("SuspiciousIndentation") -@Composable -fun AgentCard2(viewModel: AgentViewModel,agentItem: AgentItem,navController: NavHostController) { - val AppColors = LocalAppTheme.current - - // 防抖状态 - var lastClickTime by remember { mutableStateOf(0L) } - - Row( - modifier = Modifier - .fillMaxWidth() - .padding(vertical = 3.dp), - verticalAlignment = Alignment.CenterVertically - ) { - // 左侧头像 - Box( - modifier = Modifier - .size(48.dp) - .background(Color(0x00F5F5F5), RoundedCornerShape(24.dp)) - .clickable { - if (DebounceUtils.simpleDebounceClick(lastClickTime, 500L) { - viewModel.goToProfile(agentItem.openId, navController) - }) { - lastClickTime = System.currentTimeMillis() - } - }, - contentAlignment = Alignment.Center - ) { - CustomAsyncImage( - imageUrl = agentItem.avatar, - contentDescription = "Agent头像", - modifier = Modifier - .size(48.dp) - .clip(RoundedCornerShape(24.dp)), - contentScale = androidx.compose.ui.layout.ContentScale.Crop, - defaultRes = R.mipmap.group_copy - ) - } - - Spacer(modifier = Modifier.width(12.dp)) - - // 中间文字内容 - Column( - modifier = Modifier - .weight(1f) - .padding(end = 8.dp) - ) { - // 标题 - androidx.compose.material3.Text( - text = agentItem.title, - fontSize = 14.sp, - fontWeight = androidx.compose.ui.text.font.FontWeight.W600, - color = AppColors.text, - maxLines = 1, - overflow = androidx.compose.ui.text.style.TextOverflow.Ellipsis - ) - - Spacer(modifier = Modifier.height(8.dp)) - - // 描述 - androidx.compose.material3.Text( - text = agentItem.desc, - fontSize = 12.sp, - color = AppColors.secondaryText, - maxLines = 1, - overflow = androidx.compose.ui.text.style.TextOverflow.Ellipsis - ) - } - - // 右侧聊天按钮 - Box( - modifier = Modifier - .size(width = 60.dp, height = 32.dp) - .background( - color = Color(0X147c7480), - shape = RoundedCornerShape( - topStart = 14.dp, - topEnd = 14.dp, - bottomStart = 0.dp, - bottomEnd = 14.dp - ) - ) - .clickable { - if (DebounceUtils.simpleDebounceClick(lastClickTime, 500L) { - // 检查游客模式,如果是游客则跳转登录 - if (GuestLoginCheckOut.needLogin(GuestLoginCheckOutScene.CHAT_WITH_AGENT)) { - navController.navigate(NavigationRoute.Login.route) - } else { - viewModel.createSingleChat(agentItem.openId) - viewModel.goToChatAi( - agentItem.openId, - navController = navController - ) - } - }) { - lastClickTime = System.currentTimeMillis() - } - }, - contentAlignment = Alignment.Center - ) { - androidx.compose.material3.Text( - text = stringResource(R.string.chat), - fontSize = 12.sp, - color = AppColors.text, - fontWeight = androidx.compose.ui.text.font.FontWeight.W500 - ) - } - } -} -@Composable -fun ChatRoomsSection( - chatRooms: List, - navController: NavHostController -) { - val AppColors = LocalAppTheme.current - - Column( - modifier = Modifier.fillMaxWidth() - ) { - // 标题 - Row( - modifier = Modifier - .fillMaxWidth() - .padding(bottom = 16.dp), - verticalAlignment = Alignment.CenterVertically - ) { - Image( - painter = painterResource(R.mipmap.rider_pro_hot_room), - contentDescription = "chat room", - modifier = Modifier.size(28.dp) - ) - Spacer(modifier = Modifier.width(4.dp)) - androidx.compose.material3.Text( - text = stringResource(R.string.hot_rooms), - fontSize = 16.sp, - fontWeight = androidx.compose.ui.text.font.FontWeight.W900, - color = AppColors.text - ) - } - - Column( - modifier = Modifier.fillMaxWidth() - ) { - chatRooms.chunked(2).forEach { rowRooms -> - Row( - modifier = Modifier - .fillMaxWidth() - .padding(bottom = 12.dp), - horizontalArrangement = Arrangement.spacedBy(12.dp) - ) { - rowRooms.forEach { chatRoom -> - ChatRoomCard( - chatRoom = chatRoom, - navController = navController, - modifier = Modifier.weight(1f) - ) - } - } - } - } - } -} - @Composable fun ChatRoomCard( chatRoom: ChatRoom, @@ -938,7 +687,10 @@ fun ChatRoomCard( .size(cardSize) .background(AppColors.tabUnselectedBackground, RoundedCornerShape(12.dp)) .clickable(enabled = !viewModel.isJoiningRoom) { - if (!viewModel.isJoiningRoom && DebounceUtils.simpleDebounceClick(lastClickTime, 500L) { + if (!viewModel.isJoiningRoom && DebounceUtils.simpleDebounceClick( + lastClickTime, + 500L + ) { // 加入群聊房间 viewModel.joinRoom( id = chatRoom.id, @@ -953,7 +705,8 @@ fun ChatRoomCard( // 处理错误,可以显示Toast或其他提示 } ) - }) { + } + ) { lastClickTime = System.currentTimeMillis() } } @@ -967,11 +720,14 @@ fun ChatRoomCard( modifier = Modifier .width(cardSize) .height(120.dp) - .clip(RoundedCornerShape( - topStart = 12.dp, - topEnd = 12.dp, - bottomStart = 0.dp, - bottomEnd = 0.dp)), + .clip( + RoundedCornerShape( + topStart = 12.dp, + topEnd = 12.dp, + bottomStart = 0.dp, + bottomEnd = 0.dp + ) + ), contentScale = androidx.compose.ui.layout.ContentScale.Crop, defaultRes = R.mipmap.rider_pro_agent ) diff --git a/app/src/main/java/com/aiosman/ravenow/ui/index/tabs/ai/AgentViewModel.kt b/app/src/main/java/com/aiosman/ravenow/ui/index/tabs/ai/AgentViewModel.kt index 867a95e..f69e9fe 100644 --- a/app/src/main/java/com/aiosman/ravenow/ui/index/tabs/ai/AgentViewModel.kt +++ b/app/src/main/java/com/aiosman/ravenow/ui/index/tabs/ai/AgentViewModel.kt @@ -134,7 +134,7 @@ object AgentViewModel: ViewModel() { pageSize = pageSize, withWorkflow = 1, categoryIds = listOf(categoryId), - random = 1 +// random = 1 ) } else { // 获取推荐智能体,使用random=1 @@ -143,7 +143,7 @@ object AgentViewModel: ViewModel() { pageSize = pageSize, withWorkflow = 1, categoryIds = null, - random = 1 +// random = 1 ) } @@ -197,7 +197,7 @@ object AgentViewModel: ViewModel() { page = 1, pageSize = 20, isRecommended = 1, - random = "1" +// random = "1" ) if (response.isSuccessful) { val allRooms = response.body()?.list ?: emptyList() @@ -332,18 +332,17 @@ object AgentViewModel: ViewModel() { openId: String, navController: NavHostController ) { - viewModelScope.launch { - try { - val profile = userService.getUserProfileByOpenId(openId) - // 从Agent列表点击进去的一定是智能体,直接传递isAiAccount = true - navController.navigate( - NavigationRoute.AccountProfile.route - .replace("{id}", profile.id.toString()) - .replace("{isAiAccount}", "true") - ) - } catch (e: Exception) { - // swallow error to avoid crash on navigation attempt failures - } + // 直接使用openId导航,页面内的AiProfileViewModel会处理数据加载 + // 避免重复请求,因为AiProfileViewModel.loadProfile已经支持通过openId加载 + try { + navController.navigate( + NavigationRoute.AccountProfile.route + .replace("{id}", openId) + .replace("{isAiAccount}", "true") + ) + } catch (e: Exception) { + Log.e("AgentViewModel", "Navigation failed", e) + e.printStackTrace() } } diff --git a/app/src/main/java/com/aiosman/ravenow/ui/index/tabs/message/tab/GroupChatListScreen.kt b/app/src/main/java/com/aiosman/ravenow/ui/index/tabs/message/tab/GroupChatListScreen.kt index d6bd7b3..74d93ee 100644 --- a/app/src/main/java/com/aiosman/ravenow/ui/index/tabs/message/tab/GroupChatListScreen.kt +++ b/app/src/main/java/com/aiosman/ravenow/ui/index/tabs/message/tab/GroupChatListScreen.kt @@ -12,7 +12,6 @@ import androidx.compose.material.pullrefresh.PullRefreshIndicator import androidx.compose.material.pullrefresh.pullRefresh import androidx.compose.material.pullrefresh.rememberPullRefreshState import androidx.compose.material3.CircularProgressIndicator -import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -27,6 +26,7 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import androidx.compose.ui.graphics.Color import com.aiosman.ravenow.AppState import com.aiosman.ravenow.LocalAppTheme import com.aiosman.ravenow.LocalNavController @@ -153,13 +153,6 @@ fun GroupChatListScreen() { } } ) - - if (index < GroupChatListViewModel.groupChatList.size - 1) { - HorizontalDivider( - modifier = Modifier.padding(horizontal = 24.dp), - color = AppColors.divider - ) - } } if (GroupChatListViewModel.isLoading && GroupChatListViewModel.groupChatList.isNotEmpty()) { @@ -213,16 +206,16 @@ fun GroupChatItem( val AppColors = LocalAppTheme.current val chatDebouncer = rememberDebouncer() val avatarDebouncer = rememberDebouncer() - Row( modifier = Modifier .fillMaxWidth() - .padding(horizontal = 24.dp, vertical = 12.dp) + .padding(horizontal = 16.dp, vertical = 12.dp) .noRippleClickable { chatDebouncer { onChatClick(conversation) } - } + }, + verticalAlignment = Alignment.CenterVertically ) { Box { CustomAsyncImage( @@ -242,9 +235,9 @@ fun GroupChatItem( Column( modifier = Modifier - .weight(1f) - .padding(start = 12.dp) + .padding(start = 12.dp, top = 2.dp), + verticalArrangement = Arrangement.Center ) { Row( modifier = Modifier.fillMaxWidth(), @@ -252,22 +245,22 @@ fun GroupChatItem( ) { Text( text = conversation.groupName, - fontSize = 16.sp, + fontSize = 14.sp, fontWeight = FontWeight.Bold, color = AppColors.text, modifier = Modifier.weight(1f) ) - Spacer(modifier = Modifier.width(8.dp)) + Spacer(modifier = Modifier.width(6.dp)) Text( text = conversation.lastMessageTime, - fontSize = 12.sp, + fontSize = 11.sp, color = AppColors.secondaryText ) } - Spacer(modifier = Modifier.height(4.dp)) + Spacer(modifier = Modifier.height(6.dp)) Row( modifier = Modifier.fillMaxWidth(), @@ -275,29 +268,29 @@ fun GroupChatItem( ) { Text( text = "${if (conversation.isSelf) stringResource(R.string.friend_chat_me_prefix) else ""}${conversation.displayText}", - fontSize = 14.sp, + fontSize = 12.sp, color = AppColors.secondaryText, maxLines = 1, overflow = TextOverflow.Ellipsis, modifier = Modifier.weight(1f) ) - Spacer(modifier = Modifier.width(8.dp)) + Spacer(modifier = Modifier.width(10.dp)) if (conversation.unreadCount > 0) { Box( modifier = Modifier .size(if (conversation.unreadCount > 99) 24.dp else 20.dp) .background( - color = AppColors.main, + color = Color(0xFFFF3B30), shape = CircleShape ), contentAlignment = Alignment.Center ) { Text( text = if (conversation.unreadCount > 99) "99+" else conversation.unreadCount.toString(), - color = AppColors.mainText, - fontSize = if (conversation.unreadCount > 99) 9.sp else 10.sp, + color = Color.White, + fontSize = if (conversation.unreadCount > 99) 11.sp else 12.sp, fontWeight = FontWeight.Bold ) } diff --git a/app/src/main/java/com/aiosman/ravenow/ui/index/tabs/moment/tabs/recommend/PostRecommendationItem.kt b/app/src/main/java/com/aiosman/ravenow/ui/index/tabs/moment/tabs/recommend/PostRecommendationItem.kt index d1e0016..fb196f7 100644 --- a/app/src/main/java/com/aiosman/ravenow/ui/index/tabs/moment/tabs/recommend/PostRecommendationItem.kt +++ b/app/src/main/java/com/aiosman/ravenow/ui/index/tabs/moment/tabs/recommend/PostRecommendationItem.kt @@ -35,10 +35,16 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.aiosman.ravenow.R +import com.aiosman.ravenow.LocalNavController import com.aiosman.ravenow.entity.MomentEntity +import com.aiosman.ravenow.ui.NavigationRoute import com.aiosman.ravenow.ui.comment.CommentModalContent import com.aiosman.ravenow.ui.composables.CustomAsyncImage import com.aiosman.ravenow.ui.modifiers.noRippleClickable +import com.aiosman.ravenow.data.UserService +import com.aiosman.ravenow.data.UserServiceImpl +import kotlinx.coroutines.launch +import androidx.compose.runtime.rememberCoroutineScope /** * 动态推荐Item组件(post_normal) @@ -50,16 +56,37 @@ fun PostRecommendationItem( moment: MomentEntity, onLikeClick: ((MomentEntity) -> Unit)? = null, onCommentClick: ((MomentEntity) -> Unit)? = null, + onCommentAdded: ((MomentEntity) -> Unit)? = null, onFavoriteClick: ((MomentEntity) -> Unit)? = null, onShareClick: ((MomentEntity) -> Unit)? = null, modifier: Modifier = Modifier ) { val context = LocalContext.current + val navController = LocalNavController.current + val scope = rememberCoroutineScope() + val userService: UserService = UserServiceImpl() var showCommentModal by remember { mutableStateOf(false) } var sheetState = rememberModalBottomSheetState( skipPartiallyExpanded = true ) + // 导航到个人资料 + fun navigateToProfile() { + scope.launch { + try { + val profile = userService.getUserProfile(moment.authorId.toString()) + navController.navigate( + NavigationRoute.AccountProfile.route + .replace("{id}", profile.id.toString()) + .replace("{isAiAccount}", if (profile.aiAccount) "true" else "false") + ) + } catch (e: Exception) { + // 处理错误,避免崩溃 + e.printStackTrace() + } + } + } + // 图片列表 val images = moment.images val imageCount = images.size @@ -71,8 +98,9 @@ fun PostRecommendationItem( ) { // 图片显示区域(替代视频播放器) if (imageCount > 0) { - // 只显示第一张图片,优先使用 thumbnailDirectUrl - val imageUrl = images[0].thumbnailDirectUrl + // 只显示第一张图片,优先使用 smallDirectUrl + val imageUrl = images[0].smallDirectUrl + ?: images[0].thumbnailDirectUrl ?: images[0].directUrl ?: images[0].url CustomAsyncImage( @@ -104,32 +132,16 @@ fun PostRecommendationItem( .fillMaxWidth() .padding(start = 16.dp, bottom = 16.dp) ) { - // 用户头像和昵称 - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.padding(bottom = 8.dp) - ) { - Box( - modifier = Modifier - .size(40.dp) - .clip(CircleShape) - .background(Color.White.copy(alpha = 0.2f)) - ) { - CustomAsyncImage( - imageUrl = moment.avatar, - contentDescription = "用户头像", - modifier = Modifier.fillMaxSize(), - defaultRes = R.drawable.default_avatar - ) - } - Text( - text = "@${moment.nickname}", - modifier = Modifier.padding(start = 8.dp), - fontSize = 16.sp, - color = Color.White, - style = TextStyle(fontWeight = FontWeight.Bold) - ) - } + // 用户昵称 + Text( + text = "@${moment.nickname}", + modifier = Modifier + .padding(bottom = 8.dp) + .noRippleClickable { navigateToProfile() }, + fontSize = 16.sp, + color = Color.White, + style = TextStyle(fontWeight = FontWeight.Bold) + ) // 文字内容 if (!moment.momentTextContent.isNullOrEmpty()) { @@ -160,7 +172,10 @@ fun PostRecommendationItem( horizontalAlignment = Alignment.CenterHorizontally ) { // 用户头像 - UserAvatar(avatarUrl = moment.avatar) + UserAvatar( + avatarUrl = moment.avatar, + onClick = { navigateToProfile() } + ) // 点赞 VideoBtn( @@ -205,21 +220,35 @@ fun PostRecommendationItem( containerColor = Color.White, sheetState = sheetState ) { - CommentModalContent(postId = moment.id) { - // 评论添加后的回调 - } + CommentModalContent( + postId = moment.id, + commentCount = moment.commentCount, + onCommentAdded = { + onCommentAdded?.invoke(moment) + } + ) } } } @Composable -private fun UserAvatar(avatarUrl: String? = null) { +private fun UserAvatar( + avatarUrl: String? = null, + onClick: (() -> Unit)? = null +) { Box( modifier = Modifier .padding(bottom = 16.dp) .size(40.dp) .clip(CircleShape) .background(Color.White.copy(alpha = 0.2f)) + .then( + if (onClick != null) { + Modifier.noRippleClickable { onClick() } + } else { + Modifier + } + ) ) { if (avatarUrl != null && avatarUrl.isNotEmpty()) { CustomAsyncImage( diff --git a/app/src/main/java/com/aiosman/ravenow/ui/index/tabs/moment/tabs/recommend/RecommendScreen.kt b/app/src/main/java/com/aiosman/ravenow/ui/index/tabs/moment/tabs/recommend/RecommendScreen.kt index d5d2507..eb4b85d 100644 --- a/app/src/main/java/com/aiosman/ravenow/ui/index/tabs/moment/tabs/recommend/RecommendScreen.kt +++ b/app/src/main/java/com/aiosman/ravenow/ui/index/tabs/moment/tabs/recommend/RecommendScreen.kt @@ -239,10 +239,12 @@ fun RecommendScreen() { onCommentClick = { m -> if (GuestLoginCheckOut.needLogin(GuestLoginCheckOutScene.COMMENT_MOMENT)) { navController.navigate(NavigationRoute.Login.route) - } else { - scope.launch { - RecommendViewModel.onAddComment(m.id) - } + } + // 注意:不在这里增加评论数,应该在评论真正提交成功后再增加 + }, + onCommentAdded = { m -> + scope.launch { + RecommendViewModel.onAddComment(m.id) } }, onFavoriteClick = { m -> diff --git a/app/src/main/java/com/aiosman/ravenow/ui/profile/AiProfileViewModel.kt b/app/src/main/java/com/aiosman/ravenow/ui/profile/AiProfileViewModel.kt index 91fa178..0b83fbd 100644 --- a/app/src/main/java/com/aiosman/ravenow/ui/profile/AiProfileViewModel.kt +++ b/app/src/main/java/com/aiosman/ravenow/ui/profile/AiProfileViewModel.kt @@ -44,12 +44,21 @@ class AiProfileViewModel : ViewModel() { } try { - // 先通过用户ID获取基本信息,获取chatAIId - val basicProfile = userService.getUserProfile(id) - profileId = id.toInt() + // 检查id是否是纯数字(用户ID),如果不是则当作openId处理 + val isUserId = id.toIntOrNull() != null - // 使用chatAIId通过getUserProfileByOpenId获取完整信息(包含creatorProfile) - profile = userService.getUserProfileByOpenId(basicProfile.chatAIId) + if (isUserId) { + // 先通过用户ID获取基本信息,获取chatAIId + val basicProfile = userService.getUserProfile(id) + profileId = id.toInt() + + // 使用chatAIId通过getUserProfileByOpenId获取完整信息(包含creatorProfile) + profile = userService.getUserProfileByOpenId(basicProfile.chatAIId) + } else { + // 直接通过openId获取完整信息 + profile = userService.getUserProfileByOpenId(id) + profileId = profile?.id ?: 0 + } } catch (e: Exception) { Log.e("AiProfileViewModel", "Error loading profile", e) e.printStackTrace() diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 51eb923..3392851 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -46,6 +46,7 @@ zoomable = "1.6.1" camerax = "1.4.0" mlkitBarcode = "17.3.0" room = "2.8.3" +billing = "8.0.0" [libraries] accompanist-systemuicontroller = { module = "com.google.accompanist:accompanist-systemuicontroller", version.ref = "accompanistSystemuicontroller" } @@ -114,6 +115,7 @@ mlkit-barcode-scanning = { module = "com.google.mlkit:barcode-scanning", version androidx-room-runtime = { module = "androidx.room:room-runtime", version.ref = "room" } androidx-room-ktx = { module = "androidx.room:room-ktx", version.ref = "room" } androidx-room-compiler = { module = "androidx.room:room-compiler", version.ref = "room" } +billing-ktx = { module = "com.android.billingclient:billing-ktx", version.ref = "billing" } [plugins] android-application = { id = "com.android.application", version.ref = "agp" } jetbrains-kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }