feat: 增加Google支付及优化UI与性能
- **支付功能**: - 在应用启动时初始化Google Play Billing Client,为应用内购买做准备。 - 添加了`billing-ktx`依赖。 - **动态和个人主页**: - 动态推荐页:用户头像和昵称区域支持点击跳转到对应的个人资料页。 - 个人资料页:优化了用户资料加载逻辑,使其同时支持通过用户ID和OpenID加载。 - 评论功能:优化了评论交互,评论成功后才更新评论数。 - 数据模型:调整动态图片,优先使用`smallDirectUrl`以优化加载速度。 - **AI智能体页面**: - 移除API请求中的`random`参数,以改善数据缓存和一致性。 - 优化了导航到AI智能体主页的逻辑,直接传递`openId`,简化了数据请求。 - 清理了部分未使用的代码和布局。 - **群聊列表UI**: - 调整了群聊列表项的布局、字体大小和颜色,优化了视觉样式。 - 移除了列表项之间的分割线。
This commit is contained in:
@@ -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)
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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?,
|
||||
|
||||
@@ -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,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,9 +490,10 @@ fun AgentCardSquare(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
fun AgentViewPagerSection(agentItems: List<AgentItem>,viewModel: AgentViewModel) {
|
||||
fun AgentViewPagerSection(agentItems: List<AgentItem>, viewModel: AgentViewModel) {
|
||||
val AppColors = LocalAppTheme.current
|
||||
if (agentItems.isEmpty()) return
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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" }
|
||||
|
||||
Reference in New Issue
Block a user