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"
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)
}

View File

@@ -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
}
/**

View File

@@ -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?,

View File

@@ -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) }
@@ -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,7 +309,6 @@ fun Agent() {
}
// 只有当热门聊天室有数据时,才展示“发现更多”区域
if (viewModel.chatRooms.isNotEmpty()) {
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)
@Composable
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
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(
.clip(
RoundedCornerShape(
topStart = 12.dp,
topEnd = 12.dp,
bottomStart = 0.dp,
bottomEnd = 0.dp)),
bottomEnd = 0.dp
)
),
contentScale = androidx.compose.ui.layout.ContentScale.Crop,
defaultRes = R.mipmap.rider_pro_agent
)

View File

@@ -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 {
// 直接使用openId导航页面内的AiProfileViewModel会处理数据加载
// 避免重复请求因为AiProfileViewModel.loadProfile已经支持通过openId加载
try {
val profile = userService.getUserProfileByOpenId(openId)
// 从Agent列表点击进去的一定是智能体直接传递isAiAccount = true
navController.navigate(
NavigationRoute.AccountProfile.route
.replace("{id}", profile.id.toString())
.replace("{id}", openId)
.replace("{isAiAccount}", "true")
)
} 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.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
)
}

View File

@@ -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),
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(

View File

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

View File

@@ -44,12 +44,21 @@ class AiProfileViewModel : ViewModel() {
}
try {
// 检查id是否是纯数字用户ID如果不是则当作openId处理
val isUserId = id.toIntOrNull() != null
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()

View File

@@ -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" }