feat: 增加Google支付及优化UI与性能

- **支付功能**:
  - 在应用启动时初始化Google Play Billing Client,为应用内购买做准备。
  - 添加了`billing-ktx`依赖。

- **动态和个人主页**:
  - 动态推荐页:用户头像和昵称区域支持点击跳转到对应的个人资料页。
  - 个人资料页:优化了用户资料加载逻辑,使其同时支持通过用户ID和OpenID加载。
  - 评论功能:优化了评论交互,评论成功后才更新评论数。
  - 数据模型:调整动态图片,优先使用`smallDirectUrl`以优化加载速度。

- **AI智能体页面**:
  - 移除API请求中的`random`参数,以改善数据缓存和一致性。
  - 优化了导航到AI智能体主页的逻辑,直接传递`openId`,简化了数据请求。
  - 清理了部分未使用的代码和布局。

- **群聊列表UI**:
  - 调整了群聊列表项的布局、字体大小和颜色,优化了视觉样式。
  - 移除了列表项之间的分割线。
This commit is contained in:
2025-11-19 23:51:43 +08:00
parent f1e91f7639
commit 8a76bd11d9
10 changed files with 283 additions and 433 deletions

View File

@@ -17,8 +17,8 @@ android {
applicationId = "com.aiosman.ravenow" applicationId = "com.aiosman.ravenow"
minSdk = 24 minSdk = 24
targetSdk = 35 targetSdk = 35
versionCode = 1000019 versionCode = 1000021
versionName = "1.0.000.19" versionName = "1.0.000.21"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables { vectorDrawables {
@@ -140,5 +140,8 @@ dependencies {
implementation(libs.androidx.room.ktx) implementation(libs.androidx.room.ktx)
ksp(libs.androidx.room.compiler) ksp(libs.androidx.room.compiler)
// Google Play Billing
implementation(libs.billing.ktx)
} }

View File

@@ -4,6 +4,10 @@ import android.app.Application
import android.content.Context import android.content.Context
import android.content.res.Configuration import android.content.res.Configuration
import android.util.Log 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.FirebaseApp
import com.google.firebase.perf.FirebasePerformance import com.google.firebase.perf.FirebasePerformance
@@ -12,6 +16,8 @@ import com.google.firebase.perf.FirebasePerformance
*/ */
class RaveNowApplication : Application() { class RaveNowApplication : Application() {
private var billingClient: BillingClient? = null
override fun attachBaseContext(base: Context) { override fun attachBaseContext(base: Context) {
// 禁用字体缩放,固定字体大小为系统默认大小 // 禁用字体缩放,固定字体大小为系统默认大小
val configuration = Configuration(base.resources.configuration) val configuration = Configuration(base.resources.configuration)
@@ -48,6 +54,53 @@ class RaveNowApplication : Application() {
} catch (e: Exception) { } catch (e: Exception) {
Log.e("RaveNowApplication", "Firebase初始化失败在进程 $processName", e) 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
} }
/** /**

View File

@@ -35,6 +35,10 @@ data class Moment(
val commentCount: Long, val commentCount: Long,
@SerializedName("time") @SerializedName("time")
val time: String?, val time: String?,
@SerializedName("createdAt")
val createdAt: String? = null,
@SerializedName("location")
val location: String? = null,
@SerializedName("isFollowed") @SerializedName("isFollowed")
val isFollowed: Boolean, val isFollowed: Boolean,
// 新闻相关字段 // 新闻相关字段
@@ -70,11 +74,11 @@ data class Moment(
"" // 如果头像为空,使用空字符串 "" // 如果头像为空,使用空字符串
}, },
nickname = user.nickName ?: "未知用户", // 如果昵称为空,使用默认值 nickname = user.nickName ?: "未知用户", // 如果昵称为空,使用默认值
location = "Worldwide", location = location ?: "Worldwide",
time = if (time != null && time.isNotEmpty()) { time = when {
ApiClient.dateFromApiString(time) createdAt != null && createdAt.isNotEmpty() -> ApiClient.dateFromApiString(createdAt)
} else { time != null && time.isNotEmpty() -> ApiClient.dateFromApiString(time)
java.util.Date() // 如果时间为空,使用当前时间作为默认值 else -> java.util.Date() // 如果时间为空,使用当前时间作为默认值
}, },
followStatus = isFollowed, followStatus = isFollowed,
momentTextContent = textContent, momentTextContent = textContent,
@@ -204,7 +208,7 @@ data class Video(
data class User( data class User(
@SerializedName("id") @SerializedName("id")
val id: Long, val id: Long,
@SerializedName("nickName") @SerializedName("nickname")
val nickName: String?, val nickName: String?,
@SerializedName("avatar") @SerializedName("avatar")
val avatar: String?, val avatar: String?,

View File

@@ -16,7 +16,6 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.navigationBars import androidx.compose.foundation.layout.navigationBars
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.systemBars 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.wrapContentHeight
import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.foundation.pager.HorizontalPager import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Icon
import androidx.compose.material.Text import androidx.compose.material.Text
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
@@ -63,33 +58,24 @@ 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.composables.CustomAsyncImage 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.modifiers.noRippleClickable
import com.aiosman.ravenow.ui.composables.TabItem
import com.aiosman.ravenow.ui.composables.TabSpacer import com.aiosman.ravenow.ui.composables.TabSpacer
import com.aiosman.ravenow.ui.index.tabs.moment.CustomTabItem 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.AgentItem
import com.aiosman.ravenow.ui.index.tabs.moment.tabs.expolre.ExploreViewModel
import com.aiosman.ravenow.utils.DebounceUtils 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.LazyRow
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState 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.platform.LocalContext
import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties 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.draw.alpha
import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Brush
import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalConfiguration
import com.airbnb.lottie.compose.LottieAnimation import com.airbnb.lottie.compose.LottieAnimation
import com.airbnb.lottie.compose.LottieCompositionSpec import com.airbnb.lottie.compose.LottieCompositionSpec
@@ -113,10 +99,6 @@ fun Agent() {
val navigationBarPaddings = val navigationBarPaddings =
WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() + 48.dp WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() + 48.dp
val statusBarPaddingValues = WindowInsets.systemBars.asPaddingValues() 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 val viewModel: AgentViewModel = AgentViewModel
@@ -125,16 +107,6 @@ fun Agent() {
viewModel.ensureDataLoaded() viewModel.ensureDataLoaded()
} }
// 防抖状态
var lastClickTime by remember { mutableStateOf(0L) }
// 页面退出时只清理必要的资源不清理推荐Agent数据
DisposableEffect(Unit) {
onDispose {
// 只清理子页面的资源保留推荐Agent数据
// ResourceCleanupManager.cleanupPageResources("ai")
}
}
val agentItems = viewModel.agentItems val agentItems = viewModel.agentItems
var selectedTabIndex by remember { mutableStateOf(0) } var selectedTabIndex by remember { mutableStateOf(0) }
@@ -267,11 +239,19 @@ fun Agent() {
) { ) {
when { when {
selectedTabIndex == 0 -> { selectedTabIndex == 0 -> {
AgentViewPagerSection(agentItems = viewModel.agentItems.take(15), viewModel) AgentViewPagerSection(
agentItems = viewModel.agentItems.take(15),
viewModel
)
} }
selectedTabIndex in 1..viewModel.categories.size -> { selectedTabIndex in 1..viewModel.categories.size -> {
AgentViewPagerSection(agentItems = viewModel.agentItems.take(15), viewModel) AgentViewPagerSection(
agentItems = viewModel.agentItems.take(15),
viewModel
)
} }
else -> { else -> {
val shuffledAgents = viewModel.agentItems.shuffled().take(15) val shuffledAgents = viewModel.agentItems.shuffled().take(15)
AgentViewPagerSection(agentItems = shuffledAgents, viewModel) AgentViewPagerSection(agentItems = shuffledAgents, viewModel)
@@ -329,7 +309,6 @@ fun Agent() {
} }
// 只有当热门聊天室有数据时,才展示“发现更多”区域 // 只有当热门聊天室有数据时,才展示“发现更多”区域
if (viewModel.chatRooms.isNotEmpty()) {
item { Spacer(modifier = Modifier.height(20.dp)) } item { Spacer(modifier = Modifier.height(20.dp)) }
// "发现更多" 标题 - 吸顶 // "发现更多" 标题 - 吸顶
@@ -404,58 +383,7 @@ fun Agent() {
} }
} }
} }
}
}
}
@Composable
fun AgentGridLayout(
agentItems: List<AgentItem>,
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) {
Box(
modifier = Modifier.weight(1f)
) {
AgentCardSquare(
agentItem = rowItems[1],
viewModel = viewModel,
navController = navController
)
}
} else {
// 如果只有一列,添加空白占位
Spacer(modifier = Modifier.weight(1f))
}
}
}
} }
} }
@@ -562,6 +490,7 @@ fun AgentCardSquare(
} }
} }
} }
@OptIn(ExperimentalFoundationApi::class) @OptIn(ExperimentalFoundationApi::class)
@Composable @Composable
fun AgentViewPagerSection(agentItems: List<AgentItem>, viewModel: AgentViewModel) { fun AgentViewPagerSection(agentItems: List<AgentItem>, viewModel: AgentViewModel) {
@@ -711,186 +640,6 @@ fun AgentLargeCard(
} }
} }
@Composable
fun AgentPage(viewModel: AgentViewModel,agentItems: List<AgentItem>, 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<ChatRoom>,
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 @Composable
fun ChatRoomCard( fun ChatRoomCard(
chatRoom: ChatRoom, chatRoom: ChatRoom,
@@ -938,7 +687,10 @@ fun ChatRoomCard(
.size(cardSize) .size(cardSize)
.background(AppColors.tabUnselectedBackground, RoundedCornerShape(12.dp)) .background(AppColors.tabUnselectedBackground, RoundedCornerShape(12.dp))
.clickable(enabled = !viewModel.isJoiningRoom) { .clickable(enabled = !viewModel.isJoiningRoom) {
if (!viewModel.isJoiningRoom && DebounceUtils.simpleDebounceClick(lastClickTime, 500L) { if (!viewModel.isJoiningRoom && DebounceUtils.simpleDebounceClick(
lastClickTime,
500L
) {
// 加入群聊房间 // 加入群聊房间
viewModel.joinRoom( viewModel.joinRoom(
id = chatRoom.id, id = chatRoom.id,
@@ -953,7 +705,8 @@ fun ChatRoomCard(
// 处理错误可以显示Toast或其他提示 // 处理错误可以显示Toast或其他提示
} }
) )
}) { }
) {
lastClickTime = System.currentTimeMillis() lastClickTime = System.currentTimeMillis()
} }
} }
@@ -967,11 +720,14 @@ fun ChatRoomCard(
modifier = Modifier modifier = Modifier
.width(cardSize) .width(cardSize)
.height(120.dp) .height(120.dp)
.clip(RoundedCornerShape( .clip(
RoundedCornerShape(
topStart = 12.dp, topStart = 12.dp,
topEnd = 12.dp, topEnd = 12.dp,
bottomStart = 0.dp, bottomStart = 0.dp,
bottomEnd = 0.dp)), bottomEnd = 0.dp
)
),
contentScale = androidx.compose.ui.layout.ContentScale.Crop, contentScale = androidx.compose.ui.layout.ContentScale.Crop,
defaultRes = R.mipmap.rider_pro_agent defaultRes = R.mipmap.rider_pro_agent
) )

View File

@@ -134,7 +134,7 @@ object AgentViewModel: ViewModel() {
pageSize = pageSize, pageSize = pageSize,
withWorkflow = 1, withWorkflow = 1,
categoryIds = listOf(categoryId), categoryIds = listOf(categoryId),
random = 1 // random = 1
) )
} else { } else {
// 获取推荐智能体使用random=1 // 获取推荐智能体使用random=1
@@ -143,7 +143,7 @@ object AgentViewModel: ViewModel() {
pageSize = pageSize, pageSize = pageSize,
withWorkflow = 1, withWorkflow = 1,
categoryIds = null, categoryIds = null,
random = 1 // random = 1
) )
} }
@@ -197,7 +197,7 @@ object AgentViewModel: ViewModel() {
page = 1, page = 1,
pageSize = 20, pageSize = 20,
isRecommended = 1, isRecommended = 1,
random = "1" // random = "1"
) )
if (response.isSuccessful) { if (response.isSuccessful) {
val allRooms = response.body()?.list ?: emptyList() val allRooms = response.body()?.list ?: emptyList()
@@ -332,18 +332,17 @@ object AgentViewModel: ViewModel() {
openId: String, openId: String,
navController: NavHostController navController: NavHostController
) { ) {
viewModelScope.launch { // 直接使用openId导航页面内的AiProfileViewModel会处理数据加载
// 避免重复请求因为AiProfileViewModel.loadProfile已经支持通过openId加载
try { try {
val profile = userService.getUserProfileByOpenId(openId)
// 从Agent列表点击进去的一定是智能体直接传递isAiAccount = true
navController.navigate( navController.navigate(
NavigationRoute.AccountProfile.route NavigationRoute.AccountProfile.route
.replace("{id}", profile.id.toString()) .replace("{id}", openId)
.replace("{isAiAccount}", "true") .replace("{isAiAccount}", "true")
) )
} catch (e: Exception) { } catch (e: Exception) {
// swallow error to avoid crash on navigation attempt failures Log.e("AgentViewModel", "Navigation failed", e)
} e.printStackTrace()
} }
} }

View File

@@ -12,7 +12,6 @@ import androidx.compose.material.pullrefresh.PullRefreshIndicator
import androidx.compose.material.pullrefresh.pullRefresh import androidx.compose.material.pullrefresh.pullRefresh
import androidx.compose.material.pullrefresh.rememberPullRefreshState import androidx.compose.material.pullrefresh.rememberPullRefreshState
import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect 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.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import androidx.compose.ui.graphics.Color
import com.aiosman.ravenow.AppState 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
@@ -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()) { if (GroupChatListViewModel.isLoading && GroupChatListViewModel.groupChatList.isNotEmpty()) {
@@ -213,16 +206,16 @@ fun GroupChatItem(
val AppColors = LocalAppTheme.current val AppColors = LocalAppTheme.current
val chatDebouncer = rememberDebouncer() val chatDebouncer = rememberDebouncer()
val avatarDebouncer = rememberDebouncer() val avatarDebouncer = rememberDebouncer()
Row( Row(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(horizontal = 24.dp, vertical = 12.dp) .padding(horizontal = 16.dp, vertical = 12.dp)
.noRippleClickable { .noRippleClickable {
chatDebouncer { chatDebouncer {
onChatClick(conversation) onChatClick(conversation)
} }
} },
verticalAlignment = Alignment.CenterVertically
) { ) {
Box { Box {
CustomAsyncImage( CustomAsyncImage(
@@ -242,9 +235,9 @@ fun GroupChatItem(
Column( Column(
modifier = Modifier modifier = Modifier
.weight(1f) .weight(1f)
.padding(start = 12.dp) .padding(start = 12.dp, top = 2.dp),
verticalArrangement = Arrangement.Center
) { ) {
Row( Row(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
@@ -252,22 +245,22 @@ fun GroupChatItem(
) { ) {
Text( Text(
text = conversation.groupName, text = conversation.groupName,
fontSize = 16.sp, fontSize = 14.sp,
fontWeight = FontWeight.Bold, fontWeight = FontWeight.Bold,
color = AppColors.text, color = AppColors.text,
modifier = Modifier.weight(1f) modifier = Modifier.weight(1f)
) )
Spacer(modifier = Modifier.width(8.dp)) Spacer(modifier = Modifier.width(6.dp))
Text( Text(
text = conversation.lastMessageTime, text = conversation.lastMessageTime,
fontSize = 12.sp, fontSize = 11.sp,
color = AppColors.secondaryText color = AppColors.secondaryText
) )
} }
Spacer(modifier = Modifier.height(4.dp)) Spacer(modifier = Modifier.height(6.dp))
Row( Row(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
@@ -275,29 +268,29 @@ fun GroupChatItem(
) { ) {
Text( Text(
text = "${if (conversation.isSelf) stringResource(R.string.friend_chat_me_prefix) else ""}${conversation.displayText}", text = "${if (conversation.isSelf) stringResource(R.string.friend_chat_me_prefix) else ""}${conversation.displayText}",
fontSize = 14.sp, fontSize = 12.sp,
color = AppColors.secondaryText, color = AppColors.secondaryText,
maxLines = 1, maxLines = 1,
overflow = TextOverflow.Ellipsis, overflow = TextOverflow.Ellipsis,
modifier = Modifier.weight(1f) modifier = Modifier.weight(1f)
) )
Spacer(modifier = Modifier.width(8.dp)) Spacer(modifier = Modifier.width(10.dp))
if (conversation.unreadCount > 0) { if (conversation.unreadCount > 0) {
Box( Box(
modifier = Modifier modifier = Modifier
.size(if (conversation.unreadCount > 99) 24.dp else 20.dp) .size(if (conversation.unreadCount > 99) 24.dp else 20.dp)
.background( .background(
color = AppColors.main, color = Color(0xFFFF3B30),
shape = CircleShape shape = CircleShape
), ),
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ) {
Text( Text(
text = if (conversation.unreadCount > 99) "99+" else conversation.unreadCount.toString(), text = if (conversation.unreadCount > 99) "99+" else conversation.unreadCount.toString(),
color = AppColors.mainText, color = Color.White,
fontSize = if (conversation.unreadCount > 99) 9.sp else 10.sp, fontSize = if (conversation.unreadCount > 99) 11.sp else 12.sp,
fontWeight = FontWeight.Bold fontWeight = FontWeight.Bold
) )
} }

View File

@@ -35,10 +35,16 @@ import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import com.aiosman.ravenow.R import com.aiosman.ravenow.R
import com.aiosman.ravenow.LocalNavController
import com.aiosman.ravenow.entity.MomentEntity import com.aiosman.ravenow.entity.MomentEntity
import com.aiosman.ravenow.ui.NavigationRoute
import com.aiosman.ravenow.ui.comment.CommentModalContent import com.aiosman.ravenow.ui.comment.CommentModalContent
import com.aiosman.ravenow.ui.composables.CustomAsyncImage import com.aiosman.ravenow.ui.composables.CustomAsyncImage
import com.aiosman.ravenow.ui.modifiers.noRippleClickable 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 * 动态推荐Item组件post_normal
@@ -50,16 +56,37 @@ fun PostRecommendationItem(
moment: MomentEntity, moment: MomentEntity,
onLikeClick: ((MomentEntity) -> Unit)? = null, onLikeClick: ((MomentEntity) -> Unit)? = null,
onCommentClick: ((MomentEntity) -> Unit)? = null, onCommentClick: ((MomentEntity) -> Unit)? = null,
onCommentAdded: ((MomentEntity) -> Unit)? = null,
onFavoriteClick: ((MomentEntity) -> Unit)? = null, onFavoriteClick: ((MomentEntity) -> Unit)? = null,
onShareClick: ((MomentEntity) -> Unit)? = null, onShareClick: ((MomentEntity) -> Unit)? = null,
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
val context = LocalContext.current val context = LocalContext.current
val navController = LocalNavController.current
val scope = rememberCoroutineScope()
val userService: UserService = UserServiceImpl()
var showCommentModal by remember { mutableStateOf(false) } var showCommentModal by remember { mutableStateOf(false) }
var sheetState = rememberModalBottomSheetState( var sheetState = rememberModalBottomSheetState(
skipPartiallyExpanded = true 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 images = moment.images
val imageCount = images.size val imageCount = images.size
@@ -71,8 +98,9 @@ fun PostRecommendationItem(
) { ) {
// 图片显示区域(替代视频播放器) // 图片显示区域(替代视频播放器)
if (imageCount > 0) { if (imageCount > 0) {
// 只显示第一张图片,优先使用 thumbnailDirectUrl // 只显示第一张图片,优先使用 smallDirectUrl
val imageUrl = images[0].thumbnailDirectUrl val imageUrl = images[0].smallDirectUrl
?: images[0].thumbnailDirectUrl
?: images[0].directUrl ?: images[0].directUrl
?: images[0].url ?: images[0].url
CustomAsyncImage( CustomAsyncImage(
@@ -104,32 +132,16 @@ fun PostRecommendationItem(
.fillMaxWidth() .fillMaxWidth()
.padding(start = 16.dp, bottom = 16.dp) .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(
text = "@${moment.nickname}", text = "@${moment.nickname}",
modifier = Modifier.padding(start = 8.dp), modifier = Modifier
.padding(bottom = 8.dp)
.noRippleClickable { navigateToProfile() },
fontSize = 16.sp, fontSize = 16.sp,
color = Color.White, color = Color.White,
style = TextStyle(fontWeight = FontWeight.Bold) style = TextStyle(fontWeight = FontWeight.Bold)
) )
}
// 文字内容 // 文字内容
if (!moment.momentTextContent.isNullOrEmpty()) { if (!moment.momentTextContent.isNullOrEmpty()) {
@@ -160,7 +172,10 @@ fun PostRecommendationItem(
horizontalAlignment = Alignment.CenterHorizontally horizontalAlignment = Alignment.CenterHorizontally
) { ) {
// 用户头像 // 用户头像
UserAvatar(avatarUrl = moment.avatar) UserAvatar(
avatarUrl = moment.avatar,
onClick = { navigateToProfile() }
)
// 点赞 // 点赞
VideoBtn( VideoBtn(
@@ -205,21 +220,35 @@ fun PostRecommendationItem(
containerColor = Color.White, containerColor = Color.White,
sheetState = sheetState sheetState = sheetState
) { ) {
CommentModalContent(postId = moment.id) { CommentModalContent(
// 评论添加后的回调 postId = moment.id,
commentCount = moment.commentCount,
onCommentAdded = {
onCommentAdded?.invoke(moment)
} }
)
} }
} }
} }
@Composable @Composable
private fun UserAvatar(avatarUrl: String? = null) { private fun UserAvatar(
avatarUrl: String? = null,
onClick: (() -> Unit)? = null
) {
Box( Box(
modifier = Modifier modifier = Modifier
.padding(bottom = 16.dp) .padding(bottom = 16.dp)
.size(40.dp) .size(40.dp)
.clip(CircleShape) .clip(CircleShape)
.background(Color.White.copy(alpha = 0.2f)) .background(Color.White.copy(alpha = 0.2f))
.then(
if (onClick != null) {
Modifier.noRippleClickable { onClick() }
} else {
Modifier
}
)
) { ) {
if (avatarUrl != null && avatarUrl.isNotEmpty()) { if (avatarUrl != null && avatarUrl.isNotEmpty()) {
CustomAsyncImage( CustomAsyncImage(

View File

@@ -239,11 +239,13 @@ fun RecommendScreen() {
onCommentClick = { m -> onCommentClick = { m ->
if (GuestLoginCheckOut.needLogin(GuestLoginCheckOutScene.COMMENT_MOMENT)) { if (GuestLoginCheckOut.needLogin(GuestLoginCheckOutScene.COMMENT_MOMENT)) {
navController.navigate(NavigationRoute.Login.route) navController.navigate(NavigationRoute.Login.route)
} else { }
// 注意:不在这里增加评论数,应该在评论真正提交成功后再增加
},
onCommentAdded = { m ->
scope.launch { scope.launch {
RecommendViewModel.onAddComment(m.id) RecommendViewModel.onAddComment(m.id)
} }
}
}, },
onFavoriteClick = { m -> onFavoriteClick = { m ->
if (GuestLoginCheckOut.needLogin(GuestLoginCheckOutScene.LIKE_MOMENT)) { if (GuestLoginCheckOut.needLogin(GuestLoginCheckOutScene.LIKE_MOMENT)) {

View File

@@ -44,12 +44,21 @@ class AiProfileViewModel : ViewModel() {
} }
try { try {
// 检查id是否是纯数字用户ID如果不是则当作openId处理
val isUserId = id.toIntOrNull() != null
if (isUserId) {
// 先通过用户ID获取基本信息获取chatAIId // 先通过用户ID获取基本信息获取chatAIId
val basicProfile = userService.getUserProfile(id) val basicProfile = userService.getUserProfile(id)
profileId = id.toInt() profileId = id.toInt()
// 使用chatAIId通过getUserProfileByOpenId获取完整信息包含creatorProfile // 使用chatAIId通过getUserProfileByOpenId获取完整信息包含creatorProfile
profile = userService.getUserProfileByOpenId(basicProfile.chatAIId) profile = userService.getUserProfileByOpenId(basicProfile.chatAIId)
} else {
// 直接通过openId获取完整信息
profile = userService.getUserProfileByOpenId(id)
profileId = profile?.id ?: 0
}
} catch (e: Exception) { } catch (e: Exception) {
Log.e("AiProfileViewModel", "Error loading profile", e) Log.e("AiProfileViewModel", "Error loading profile", e)
e.printStackTrace() e.printStackTrace()

View File

@@ -46,6 +46,7 @@ zoomable = "1.6.1"
camerax = "1.4.0" camerax = "1.4.0"
mlkitBarcode = "17.3.0" mlkitBarcode = "17.3.0"
room = "2.8.3" room = "2.8.3"
billing = "8.0.0"
[libraries] [libraries]
accompanist-systemuicontroller = { module = "com.google.accompanist:accompanist-systemuicontroller", version.ref = "accompanistSystemuicontroller" } 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-runtime = { module = "androidx.room:room-runtime", version.ref = "room" }
androidx-room-ktx = { module = "androidx.room:room-ktx", 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" } androidx-room-compiler = { module = "androidx.room:room-compiler", version.ref = "room" }
billing-ktx = { module = "com.android.billingclient:billing-ktx", version.ref = "billing" }
[plugins] [plugins]
android-application = { id = "com.android.application", version.ref = "agp" } android-application = { id = "com.android.application", version.ref = "agp" }
jetbrains-kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } jetbrains-kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }