5 Commits
main ... atm2

Author SHA1 Message Date
8a76bd11d9 feat: 增加Google支付及优化UI与性能
- **支付功能**:
  - 在应用启动时初始化Google Play Billing Client,为应用内购买做准备。
  - 添加了`billing-ktx`依赖。

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

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

- **群聊列表UI**:
  - 调整了群聊列表项的布局、字体大小和颜色,优化了视觉样式。
  - 移除了列表项之间的分割线。
2025-11-19 23:51:43 +08:00
f1e91f7639 Merge branch 'main' into atm2 2025-11-18 21:46:59 +08:00
4feca77924 Merge branch 'main' into atm2
# Conflicts:
#	app/src/main/java/com/aiosman/ravenow/ui/index/tabs/message/tab/AllChatListScreen.kt
2025-11-18 01:29:34 +08:00
b12f359da1 优化会话列表的数据加载和合并逻辑
- 在 `AndroidManifest.xml` 中添加 `com.android.vending.BILLING` 权限。
- 重构 `AllChatListScreen.kt` 的数据加载机制,通过 `LaunchedEffect` 监听各个 `ViewModel` 的列表变化,并自动合并数据。
- 将原有的数据刷新和合并逻辑拆分为 `refreshAllData` 和 `combineAllData` 两个函数,提高了代码的清晰度和复用性。
- 优化了下拉刷新和初次加载的逻辑,确保在所有数据源加载完成后才更新UI状态,提升了用户体验。
2025-11-14 16:47:15 +08:00
8901792561 新增通知类消息展示及解析逻辑
- 新增 `OpenIMMessageType.kt`,用于统一管理 OpenIM 的消息类型常量,并提供判断是否为通知类型消息的辅助函数。
- 新增 `NotificationMessageHelper.kt`,用于根据不同的通知类型生成用户友好的提示文本。
- 新增 `NotificationMessageItem.kt` Composable 组件,用于在聊天界面中展示居中样式的通知消息。
- 在 `ChatItem` 实体中增加 `isNotification` 字段,以标识消息是否为通知类型。
- 更新 `MessageParser` 和 `ChatItem` 的转换逻辑,以正确解析和处理通知消息,确保其在会话列表和聊天界面中正确显示。
- 在 `GroupChatScreen`, `ChatScreen`, `ChatAiScreen` 中,根据 `isNotification` 字段调用新的 `NotificationMessageItem` 组件来渲染通知消息。
- 修正获取群组会话 ID 时可能存在的 `s` 前缀缺失问题。
2025-11-14 15:05:11 +08:00
178 changed files with 2771 additions and 3508 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

@@ -8,6 +8,7 @@
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" /> <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.READ_PHONE_STATE" /> <uses-permission android:name="android.permission.READ_PHONE_STATE" />
<uses-permission android:name="android.permission.CAMERA" /> <uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="com.android.vending.BILLING" />
<uses-feature android:name="android.hardware.camera.any" android:required="false" /> <uses-feature android:name="android.hardware.camera.any" android:required="false" />
<application <application
@@ -54,7 +55,7 @@
android:theme="@style/Theme.App.Starting" android:theme="@style/Theme.App.Starting"
android:screenOrientation="portrait" android:screenOrientation="portrait"
android:windowSoftInputMode="adjustResize" android:windowSoftInputMode="adjustResize"
android:configChanges="fontScale|orientation|screenSize|keyboardHidden|uiMode"> android:configChanges="fontScale|orientation|screenSize|keyboardHidden">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" /> <category android:name="android.intent.category.LAUNCHER" />

View File

@@ -73,10 +73,6 @@ class MainActivity : ComponentActivity() {
val config = Configuration(newConfig) val config = Configuration(newConfig)
config.fontScale = 1.0f config.fontScale = 1.0f
super.onConfigurationChanged(config) super.onConfigurationChanged(config)
val isNightMode = (newConfig.uiMode and Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES
if (AppState.darkMode != isNightMode) {
syncDarkModeWithSystem(isNightMode)
}
} }
// 请求通知权限 // 请求通知权限
@@ -133,7 +129,9 @@ class MainActivity : ComponentActivity() {
JPushInterface.init(this) JPushInterface.init(this)
updateWindowBackground(AppState.darkMode) if (AppState.darkMode) {
window.decorView.setBackgroundColor(android.graphics.Color.BLACK)
}
enableEdgeToEdge() enableEdgeToEdge()
scope.launch { scope.launch {
@@ -271,22 +269,8 @@ class MainActivity : ComponentActivity() {
notificationManager.createNotificationChannel(channel) notificationManager.createNotificationChannel(channel)
} }
} }
private fun syncDarkModeWithSystem(isNightMode: Boolean) {
AppState.darkMode = isNightMode
AppState.appTheme = if (isNightMode) DarkThemeColors() else LightThemeColors()
AppStore.saveDarkMode(isNightMode)
updateWindowBackground(isNightMode)
}
private fun updateWindowBackground(isDarkMode: Boolean) {
window.decorView.setBackgroundColor(
if (isDarkMode) android.graphics.Color.BLACK else android.graphics.Color.WHITE
)
}
} }
val LocalNavController = compositionLocalOf<NavHostController> { val LocalNavController = compositionLocalOf<NavHostController> {
error("NavController not provided") error("NavController not provided")
} }

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

@@ -545,13 +545,7 @@ class AccountServiceImpl : AccountService {
val bannerField: MultipartBody.Part? = banner?.let { val bannerField: MultipartBody.Part? = banner?.let {
createMultipartBody(it.file, it.filename, "banner") createMultipartBody(it.file, it.filename, "banner")
} }
val resp = ApiClient.api.updateProfile(avatarField, bannerField, nicknameField, bioField) ApiClient.api.updateProfile(avatarField, bannerField, nicknameField, bioField)
if (!resp.isSuccessful) {
parseErrorResponse(resp.errorBody())?.let {
throw it.toServiceException()
}
throw ServiceException("Failed to update profile")
}
} }
override suspend fun registerUserWithPassword(loginName: String, password: String) { override suspend fun registerUserWithPassword(loginName: String, password: String) {

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

@@ -4,6 +4,7 @@ import android.content.Context
import android.icu.util.Calendar import android.icu.util.Calendar
import com.aiosman.ravenow.ConstVars import com.aiosman.ravenow.ConstVars
import com.aiosman.ravenow.exp.formatChatTime import com.aiosman.ravenow.exp.formatChatTime
import com.aiosman.ravenow.utils.NotificationMessageHelper
import com.google.gson.annotations.SerializedName import com.google.gson.annotations.SerializedName
import io.openim.android.sdk.models.Message import io.openim.android.sdk.models.Message
import io.openim.android.sdk.models.PictureElem import io.openim.android.sdk.models.PictureElem
@@ -21,7 +22,8 @@ data class ChatItem(
val textDisplay: String = "", val textDisplay: String = "",
val msgId: String, // Add this property val msgId: String, // Add this property
var showTimestamp: Boolean = false, var showTimestamp: Boolean = false,
var showTimeDivider: Boolean = false var showTimeDivider: Boolean = false,
val isNotification: Boolean = false // 标识是否为通知类型消息
) { ) {
companion object { companion object {
// OpenIM 消息类型常量 // OpenIM 消息类型常量
@@ -36,6 +38,32 @@ data class ChatItem(
val calendar = Calendar.getInstance() val calendar = Calendar.getInstance()
calendar.timeInMillis = timestamp calendar.timeInMillis = timestamp
// 检查是否为通知类型消息
// 1. 检查消息类型是否为通知类型
// 2. 检查发送者ID是否为系统账户如 "imAdmin"、"administrator" 等)
val sendID = message.sendID ?: ""
val isSystemAccount = sendID == "imAdmin" || sendID == "administrator" || sendID.isEmpty()
val isNotificationType = OpenIMMessageType.isNotification(message.contentType)
val isNotification = isNotificationType || isSystemAccount
// 如果是通知类型消息,使用特殊处理
if (isNotification) {
val notificationText = NotificationMessageHelper.getNotificationText(message)
return ChatItem(
message = notificationText,
avatar = "", // 通知消息不显示头像
time = calendar.time.formatChatTime(context),
userId = sendID.ifEmpty { "system" },
nickname = "", // 通知消息不显示昵称
timestamp = timestamp,
imageList = emptyList<PictureInfo>().toMutableList(),
messageType = message.contentType,
textDisplay = notificationText,
msgId = message.clientMsgID,
isNotification = true
)
}
var faceAvatar = avatar var faceAvatar = avatar
if (faceAvatar == null) { if (faceAvatar == null) {
faceAvatar = "${ConstVars.BASE_SERVER}${message.senderFaceUrl}" faceAvatar = "${ConstVars.BASE_SERVER}${message.senderFaceUrl}"
@@ -62,7 +90,8 @@ data class ChatItem(
).toMutableList(), ).toMutableList(),
messageType = MESSAGE_TYPE_IMAGE, messageType = MESSAGE_TYPE_IMAGE,
textDisplay = "Image", textDisplay = "Image",
msgId = message.clientMsgID msgId = message.clientMsgID,
isNotification = false
) )
} }
return null return null
@@ -79,7 +108,8 @@ data class ChatItem(
imageList = emptyList<PictureInfo>().toMutableList(), imageList = emptyList<PictureInfo>().toMutableList(),
messageType = MESSAGE_TYPE_TEXT, messageType = MESSAGE_TYPE_TEXT,
textDisplay = message.textElem?.content ?: "Unsupported message type", textDisplay = message.textElem?.content ?: "Unsupported message type",
msgId = message.clientMsgID msgId = message.clientMsgID,
isNotification = false
) )
} }

View File

@@ -410,16 +410,7 @@ class MomentLoader : DataLoader<MomentEntity,MomentLoaderExtraArgs>() {
fun updateMomentLike(id: Int,isLike:Boolean) { fun updateMomentLike(id: Int,isLike:Boolean) {
this.list = this.list.map { momentItem -> this.list = this.list.map { momentItem ->
if (momentItem.id == id) { if (momentItem.id == id) {
// 只有当状态发生变化时才更新计数,避免重复更新 momentItem.copy(likeCount = momentItem.likeCount + if (isLike) 1 else -1, liked = isLike)
val countDelta = if (momentItem.liked != isLike) {
if (isLike) 1 else -1
} else {
0
}
momentItem.copy(
likeCount = (momentItem.likeCount + countDelta).coerceAtLeast(0),
liked = isLike
)
} else { } else {
momentItem momentItem
} }
@@ -430,16 +421,7 @@ class MomentLoader : DataLoader<MomentEntity,MomentLoaderExtraArgs>() {
fun updateFavoriteCount(id: Int,isFavorite:Boolean) { fun updateFavoriteCount(id: Int,isFavorite:Boolean) {
this.list = this.list.map { momentItem -> this.list = this.list.map { momentItem ->
if (momentItem.id == id) { if (momentItem.id == id) {
// 只有当状态发生变化时才更新计数,避免重复更新 momentItem.copy(favoriteCount = momentItem.favoriteCount + if (isFavorite) 1 else -1, isFavorite = isFavorite)
val countDelta = if (momentItem.isFavorite != isFavorite) {
if (isFavorite) 1 else -1
} else {
0
}
momentItem.copy(
favoriteCount = (momentItem.favoriteCount + countDelta).coerceAtLeast(0),
isFavorite = isFavorite
)
} else { } else {
momentItem momentItem
} }

View File

@@ -0,0 +1,198 @@
package com.aiosman.ravenow.entity
/**
* OpenIM 消息类型常量
* 对应 OpenIM SDK 的 ContentType 枚举值
*/
object OpenIMMessageType {
// ========== 基础消息类型 ==========
/** 文本消息 */
const val TEXT = 101
/** 图片消息 */
const val IMAGE = 102
/** 语音消息 */
const val VOICE = 103
/** 视频消息 */
const val VIDEO = 104
/** 文件消息 */
const val FILE = 105
/** @消息 */
const val AT = 106
/** 合并消息 */
const val MERGE = 107
/** 名片消息 */
const val CARD = 108
/** 位置消息 */
const val LOCATION = 109
/** 自定义消息 */
const val CUSTOM = 110
/** 输入状态 */
const val TYPING = 113
/** 引用消息 */
const val QUOTE = 114
/** 表情消息 */
const val EMOJI = 115
// ========== 通知消息类型 ==========
/** 双方成为好友通知 */
const val FRIEND_ADDED = 1201
/** 系统通知 */
const val SYSTEM_NOTIFICATION = 1400
// ========== 群通知消息类型 ==========
/** 群创建通知 */
const val GROUP_CREATED = 1501
/** 群信息改变通知 */
const val GROUP_INFO_CHANGED = 1502
/** 群成员退出通知 */
const val GROUP_MEMBER_QUIT = 1504
/** 群主更换通知 */
const val GROUP_OWNER_CHANGED = 1507
/** 群成员被踢通知 */
const val GROUP_MEMBER_KICKED = 1508
/** 邀请群成员通知 */
const val GROUP_MEMBER_INVITED = 1509
/** 群成员进群通知 */
const val GROUP_MEMBER_JOINED = 1510
/** 解散群通知 */
const val GROUP_DISMISSED = 1511
/** 群成员禁言通知 */
const val GROUP_MEMBER_MUTED = 1512
/** 取消群成员禁言通知 */
const val GROUP_MEMBER_UNMUTED = 1513
/** 群禁言通知 */
const val GROUP_MUTED = 1514
/** 取消群禁言通知 */
const val GROUP_UNMUTED = 1515
/** 群公告改变通知 */
const val GROUP_ANNOUNCEMENT_CHANGED = 1519
/** 群名称改变通知 */
const val GROUP_NAME_CHANGED = 1520
// ========== 其他通知类型 ==========
/** 阅后即焚开启或关闭通知 */
const val SNAPCHAT_TOGGLE = 1701
/** 撤回消息通知 */
const val MESSAGE_REVOKED = 2101
/**
* 获取消息类型的描述
*/
fun getDescription(type: Int): String {
return when (type) {
TEXT -> "文本消息"
IMAGE -> "图片消息"
VOICE -> "语音消息"
VIDEO -> "视频消息"
FILE -> "文件消息"
AT -> "@消息"
MERGE -> "合并消息"
CARD -> "名片消息"
LOCATION -> "位置消息"
CUSTOM -> "自定义消息"
TYPING -> "输入状态"
QUOTE -> "引用消息"
EMOJI -> "表情消息"
FRIEND_ADDED -> "双方成为好友通知"
SYSTEM_NOTIFICATION -> "系统通知"
GROUP_CREATED -> "群创建通知"
GROUP_INFO_CHANGED -> "群信息改变通知"
GROUP_MEMBER_QUIT -> "群成员退出通知"
GROUP_OWNER_CHANGED -> "群主更换通知"
GROUP_MEMBER_KICKED -> "群成员被踢通知"
GROUP_MEMBER_INVITED -> "邀请群成员通知"
GROUP_MEMBER_JOINED -> "群成员进群通知"
GROUP_DISMISSED -> "解散群通知"
GROUP_MEMBER_MUTED -> "群成员禁言通知"
GROUP_MEMBER_UNMUTED -> "取消群成员禁言通知"
GROUP_MUTED -> "群禁言通知"
GROUP_UNMUTED -> "取消群禁言通知"
GROUP_ANNOUNCEMENT_CHANGED -> "群公告改变通知"
GROUP_NAME_CHANGED -> "群名称改变通知"
SNAPCHAT_TOGGLE -> "阅后即焚开启或关闭通知"
MESSAGE_REVOKED -> "撤回消息通知"
else -> "未知消息类型($type)"
}
}
/**
* 判断是否为通知类型消息
*/
fun isNotification(type: Int): Boolean {
return type in listOf(
FRIEND_ADDED,
SYSTEM_NOTIFICATION,
GROUP_CREATED,
GROUP_INFO_CHANGED,
GROUP_MEMBER_QUIT,
GROUP_OWNER_CHANGED,
GROUP_MEMBER_KICKED,
GROUP_MEMBER_INVITED,
GROUP_MEMBER_JOINED,
GROUP_DISMISSED,
GROUP_MEMBER_MUTED,
GROUP_MEMBER_UNMUTED,
GROUP_MUTED,
GROUP_UNMUTED,
GROUP_ANNOUNCEMENT_CHANGED,
GROUP_NAME_CHANGED,
SNAPCHAT_TOGGLE,
MESSAGE_REVOKED
)
}
/**
* 判断是否为群通知类型消息
*/
fun isGroupNotification(type: Int): Boolean {
return type in listOf(
GROUP_CREATED,
GROUP_INFO_CHANGED,
GROUP_MEMBER_QUIT,
GROUP_OWNER_CHANGED,
GROUP_MEMBER_KICKED,
GROUP_MEMBER_INVITED,
GROUP_MEMBER_JOINED,
GROUP_DISMISSED,
GROUP_MEMBER_MUTED,
GROUP_MEMBER_UNMUTED,
GROUP_MUTED,
GROUP_UNMUTED,
GROUP_ANNOUNCEMENT_CHANGED,
GROUP_NAME_CHANGED
)
}
}

View File

@@ -1,6 +1,5 @@
package com.aiosman.ravenow.entity package com.aiosman.ravenow.entity
import android.util.Log
import androidx.paging.PagingSource import androidx.paging.PagingSource
import androidx.paging.PagingState import androidx.paging.PagingState
import com.aiosman.ravenow.data.ListContainer import com.aiosman.ravenow.data.ListContainer
@@ -273,41 +272,19 @@ class RoomRemoteDataSource {
pageSize: Int = 20, pageSize: Int = 20,
search: String search: String
): ListContainer<RoomEntity>? { ): ListContainer<RoomEntity>? {
return try { val resp = ApiClient.api.getRooms(
val resp = ApiClient.api.getRooms( page = pageNumber,
page = pageNumber, pageSize = pageSize,
pageSize = pageSize, search = search,
search = search, roomType = "public" // 搜索时只显示公有房间
roomType = "public" // 搜索时只显示公有房间 )
) val body = resp.body() ?: return null
if (!resp.isSuccessful) { return ListContainer(
// API 调用失败,返回 null total = body.total,
return null page = pageNumber,
} pageSize = pageSize,
val body = resp.body() ?: return null list = body.list.map { it.toRoomtEntity() }
)
// 安全地转换数据,过滤掉转换失败的项目
val roomList = body.list.mapNotNull { room ->
try {
room.toRoomtEntity()
} catch (e: Exception) {
// 如果某个房间数据转换失败,记录错误但继续处理其他房间
Log.e("RoomRemoteDataSource", "Failed to convert room: ${room.id}", e)
null
}
}
ListContainer(
total = body.total,
page = pageNumber,
pageSize = pageSize,
list = roomList
)
} catch (e: Exception) {
// 捕获所有异常,返回 null 让 PagingSource 处理
Log.e("RoomRemoteDataSource", "searchRooms error", e)
null
}
} }
} }
@@ -326,31 +303,17 @@ class RoomSearchPagingSource(
pageSize = params.loadSize, pageSize = params.loadSize,
search = keyword search = keyword
) )
if (rooms == null) { LoadResult.Page(
// API 调用失败,返回空列表 data = rooms?.list ?: listOf(),
LoadResult.Page( prevKey = if (currentPage == 1) null else currentPage - 1,
data = emptyList(), nextKey = if (rooms?.list?.isNotEmpty() == true) currentPage + 1 else null
prevKey = if (currentPage == 1) null else currentPage - 1, )
nextKey = null } catch (exception: IOException) {
)
} else {
LoadResult.Page(
data = rooms.list,
prevKey = if (currentPage == 1) null else currentPage - 1,
nextKey = if (rooms.list.isNotEmpty() && rooms.list.size >= params.loadSize) currentPage + 1 else null
)
}
} catch (exception: Exception) {
// 捕获所有异常,包括 IOException、ServiceException 等
LoadResult.Error(exception) LoadResult.Error(exception)
} }
} }
override fun getRefreshKey(state: PagingState<Int, RoomEntity>): Int? { override fun getRefreshKey(state: PagingState<Int, RoomEntity>): Int? {
// 更健壮的实现:根据 anchorPosition 计算刷新键 return state.anchorPosition
return state.anchorPosition?.let { anchorPosition ->
state.closestPageToPosition(anchorPosition)?.prevKey?.plus(1)
?: state.closestPageToPosition(anchorPosition)?.nextKey?.minus(1)
}
} }
} }

View File

@@ -2,7 +2,6 @@ package com.aiosman.ravenow
import android.content.Context import android.content.Context
import android.content.SharedPreferences import android.content.SharedPreferences
import android.content.res.Configuration
import com.google.android.gms.auth.api.signin.GoogleSignInOptions import com.google.android.gms.auth.api.signin.GoogleSignInOptions
/** /**
@@ -25,16 +24,11 @@ object AppStore {
.requestEmail() .requestEmail()
.build() .build()
googleSignInOptions = gso googleSignInOptions = gso
// apply dark mode - 如果用户未手动设置,优先跟随系统 // apply dark mode
val hasUserPreference = sharedPreferences.contains("darkMode") if (sharedPreferences.getBoolean("darkMode", false)) {
val resolvedDarkMode = if (hasUserPreference) { AppState.darkMode = true
sharedPreferences.getBoolean("darkMode", false) AppState.appTheme = DarkThemeColors()
} else {
val currentNightMode = context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK
currentNightMode == Configuration.UI_MODE_NIGHT_YES
} }
AppState.darkMode = resolvedDarkMode
AppState.appTheme = if (resolvedDarkMode) DarkThemeColors() else LightThemeColors()
// load chat background // load chat background
val savedBgUrl = sharedPreferences.getString("chatBackgroundUrl", null) val savedBgUrl = sharedPreferences.getString("chatBackgroundUrl", null)

View File

@@ -2,7 +2,6 @@ package com.aiosman.ravenow.ui.about
import androidx.compose.foundation.Image import androidx.compose.foundation.Image
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
@@ -41,21 +40,22 @@ fun AboutScreen() {
modifier = Modifier.padding(horizontal = 24.dp, vertical = 16.dp) modifier = Modifier.padding(horizontal = 24.dp, vertical = 16.dp)
) { ) {
NoticeScreenHeader( NoticeScreenHeader(
title = stringResource(R.string.about_paipai), title = stringResource(R.string.about_rave_now),
moreIcon = false moreIcon = false
) )
} }
Column( Column(
modifier = Modifier modifier = Modifier
.weight(1f) .weight(1f)
.fillMaxWidth(), .fillMaxWidth()
horizontalAlignment = Alignment.CenterHorizontally, .padding(start = 24.dp),
verticalArrangement = Arrangement.Center horizontalAlignment = Alignment.CenterHorizontally
) { ) {
// app icondww Spacer(modifier = Modifier.height(48.dp))
// app icon
Box { Box {
Image( Image(
painter = painterResource(id = R.mipmap.invalid_name), painter = painterResource(id = R.mipmap.rider_pro_color_logo_next),
contentDescription = "app icon", contentDescription = "app icon",
modifier = Modifier.size(80.dp) modifier = Modifier.size(80.dp)
) )
@@ -63,7 +63,7 @@ fun AboutScreen() {
Spacer(modifier = Modifier.height(24.dp)) Spacer(modifier = Modifier.height(24.dp))
// app name // app name
Text( Text(
text = stringResource(R.string.paipai), text = "Rave Now".uppercase(),
fontSize = 24.sp, fontSize = 24.sp,
color = appColors.text, color = appColors.text,
fontWeight = FontWeight.ExtraBold fontWeight = FontWeight.ExtraBold

View File

@@ -1,15 +0,0 @@
package com.aiosman.ravenow.ui.account
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
@Composable
fun MbtiBottomSheetHost() {
val show = MbtiSheetManager.visible.collectAsState(false).value
if (show) {
MbtiSelectBottomSheet(
onClose = { MbtiSheetManager.close() }
)
}
}

View File

@@ -1,62 +1,42 @@
package com.aiosman.ravenow.ui.account package com.aiosman.ravenow.ui.account
import androidx.compose.foundation.Image
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.asPaddingValues
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxHeight
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.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.width import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.Surface
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.unit.Velocity
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
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.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Check
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
import com.aiosman.ravenow.R import com.aiosman.ravenow.R
import com.aiosman.ravenow.ui.modifiers.noRippleClickable import com.aiosman.ravenow.ui.comment.NoticeScreenHeader
// MBTI类型列表 // MBTI类型列表
val MBTI_TYPES = listOf( val MBTI_TYPES = listOf(
@@ -66,318 +46,96 @@ val MBTI_TYPES = listOf(
"ISTP", "ISFP", "ESTP", "ESFP" "ISTP", "ISFP", "ESTP", "ESFP"
) )
fun getMbtiImageResId(mbti: String, isDarkMode: Boolean): Int {
return when {
isDarkMode && mbti == "ENTP" -> R.mipmap.anmbti_entp
isDarkMode && mbti == "ESTP" -> R.mipmap.anmbti_estp
isDarkMode && mbti == "ENTJ" -> R.mipmap.anmbti_entj
else -> when (mbti) {
"INTJ" -> R.mipmap.mbti_intj
"INTP" -> R.mipmap.mbti_intp
"ENTJ" -> R.mipmap.mbti_entj
"ENTP" -> R.mipmap.mbti_entp
"INFJ" -> R.mipmap.mbti_infj
"INFP" -> R.mipmap.mbti_infp
"ENFJ" -> R.mipmap.mbti_enfj
"ENFP" -> R.mipmap.mbti_enfp
"ISTJ" -> R.mipmap.mbti_istj
"ISFJ" -> R.mipmap.mbti_isfj
"ESTJ" -> R.mipmap.mbti_estj
"ESFJ" -> R.mipmap.mbti_esfj
"ISTP" -> R.mipmap.mbti_istp
"ISFP" -> R.mipmap.mbti_isfp
"ESTP" -> R.mipmap.mbti_estp
"ESFP" -> R.mipmap.mbti_esfp
else -> R.mipmap.xingzuo
}
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun MbtiSelectBottomSheet( fun MbtiSelectScreen() {
onClose: () -> Unit val navController = LocalNavController.current
) {
val appColors = LocalAppTheme.current val appColors = LocalAppTheme.current
val isDarkMode = AppState.darkMode
val model = AccountEditViewModel val model = AccountEditViewModel
val currentMbti = model.mbti val currentMbti = model.mbti
val sheetBackgroundColor = if (isDarkMode) {
appColors.secondaryBackground
} else {
Color(0xFFFFFFFF)
}
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) Column(
modifier = Modifier
ModalBottomSheet( .fillMaxSize()
onDismissRequest = onClose, .background(appColors.profileBackground)
sheetState = sheetState,
containerColor = Color.Transparent,
shape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp),
dragHandle = {}
) { ) {
// 头部
Box( Box(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.fillMaxHeight() .padding(vertical = 16.dp)
.padding(top = 8.dp)
) { ) {
Surface( NoticeScreenHeader(
modifier = Modifier title = stringResource(R.string.choose_mbti),
.fillMaxWidth() moreIcon = false
.fillMaxHeight() )
.clip(RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp)), }
color = sheetBackgroundColor,
tonalElevation = 0.dp, // 列表
shadowElevation = 0.dp, LazyColumn(
) { modifier = Modifier.fillMaxSize(),
Column( contentPadding = androidx.compose.foundation.layout.PaddingValues(horizontal = 16.dp, vertical = 8.dp)
modifier = Modifier ) {
.fillMaxWidth() items(MBTI_TYPES) { mbti ->
.fillMaxHeight() MBTIItem(
.padding(horizontal = 16.dp, vertical = 8.dp) mbti = mbti,
) { isSelected = mbti == currentMbti,
// 头部 onClick = {
Box( model.mbti = mbti
modifier = Modifier // 立即保存到本地存储,确保选择后立即生效
.fillMaxWidth() AppState.UserId?.let { uid ->
.height(48.dp), com.aiosman.ravenow.AppStore.setUserMbti(uid, mbti)
contentAlignment = Alignment.Center
) {
val cancelButtonGradientColors = if (isDarkMode) {
listOf(
Color(0xFF3A3A3C),
Color(0xFF2C2C2E)
)
} else {
listOf(
Color(0xFFFFFFFF),
Color(0xFFF8F8F8)
)
} }
val cancelButtonContentColor = if (isDarkMode) Color(0xFFFFFFFF) else Color(0xFF404040) navController.navigateUp()
// 左上角返回按钮:整体「箭头 + 取消」在按钮内居中
Box(
modifier = Modifier
.align(Alignment.CenterStart)
.width(91.dp)
.height(44.dp)
.clip(RoundedCornerShape(1000.dp))
.background(
brush = Brush.linearGradient(
colors = cancelButtonGradientColors
)
)
.noRippleClickable { onClose() },
contentAlignment = Alignment.Center
) {
Row(
modifier = Modifier
.padding(horizontal = 12.dp, vertical = 4.dp),
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically
) {
Image(
painter = painterResource(id = R.drawable.rider_pro_back_icon),
contentDescription = null,
modifier = Modifier.size(17.dp),
colorFilter = ColorFilter.tint(cancelButtonContentColor)
)
Spacer(modifier = Modifier.width(4.dp))
Text(
text = "取消",
fontSize = 17.sp,
fontWeight = FontWeight.Medium,
color = cancelButtonContentColor,
textAlign = TextAlign.Center,
maxLines = 1,
overflow = TextOverflow.Clip
)
}
}
// 中间标题
Text(
text = stringResource(R.string.choose_mbti),
color = appColors.text,
fontSize = 20.sp,
fontWeight = FontWeight.Bold,
textAlign = TextAlign.Center
)
} }
)
Spacer(Modifier.height(12.dp)) Spacer(modifier = Modifier.height(8.dp))
// NestedScroll阻止滚动事件向上传到 BottomSheet
val nestedScrollConnection = remember {
object : NestedScrollConnection {
override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
return Offset.Zero
}
override fun onPostScroll(
consumed: Offset,
available: Offset,
source: NestedScrollSource
): Offset {
return available
}
override suspend fun onPreFling(available: Velocity): Velocity {
return Velocity.Zero
}
override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity {
return available
}
}
}
val descriptionBackgroundColor = if (isDarkMode) {
Color(0xFF2A2A2A)
} else {
Color(0xFFFAF9FB)
}
// 列表:上面是说明文字,下面是 MBTI 网格
LazyColumn(
modifier = Modifier
.fillMaxWidth()
.weight(1f)
.nestedScroll(nestedScrollConnection),
contentPadding = PaddingValues(
start = 8.dp,
top = 0.dp,
end = 8.dp,
bottom = 8.dp
),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
// 说明文字
item {
Box(
modifier = Modifier
.fillMaxWidth()
.clip(RoundedCornerShape(16.dp))
.background(descriptionBackgroundColor)
.padding(horizontal = 16.dp, vertical = 12.dp)
) {
Text(
text = stringResource(R.string.mbti_description),
color = appColors.text,
fontSize = 14.sp,
lineHeight = 20.sp
)
}
}
// MBTI 类型网格2 列)
itemsIndexed(MBTI_TYPES.chunked(2)) { rowIndex, rowItems ->
Row(
modifier = Modifier
.fillMaxWidth()
.padding(
bottom = if (rowIndex < MBTI_TYPES.chunked(2).size - 1) 10.dp else 0.dp
),
horizontalArrangement = Arrangement.spacedBy(10.dp)
) {
rowItems.forEach { mbti ->
Box(
modifier = Modifier.weight(1f)
) {
MbtiItem(
mbti = mbti,
isSelected = mbti == currentMbti,
onClick = {
model.mbti = mbti
onClose()
}
)
}
}
if (rowItems.size == 1) {
Spacer(modifier = Modifier.weight(1f))
}
}
}
}
}
} }
} }
} }
} }
// 保留原有的 MbtiSelectScreen 用于导航路由(如果需要)
@OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun MbtiSelectScreen() { fun MBTIItem(
val navController = LocalNavController.current
MbtiSelectBottomSheet(
onClose = {
navController.navigateUp()
}
)
}
@Composable
fun MbtiItem(
mbti: String, mbti: String,
isSelected: Boolean, isSelected: Boolean,
onClick: () -> Unit onClick: () -> Unit
) { ) {
val appColors = LocalAppTheme.current val appColors = LocalAppTheme.current
val isDarkMode = AppState.darkMode
// 卡片背景色 Box(
val cardBackgroundColor = if (isDarkMode) {
Color(0xFF2A2A2A) // 比 secondaryBackground (0xFF1C1C1C) 更亮的灰色
} else {
Color(0xFFFAF9FB)
}
Column(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.aspectRatio(1.1f) .clip(RoundedCornerShape(16.dp))
.shadow( .background(if (isSelected) appColors.main.copy(alpha = 0.1f) else Color.White)
elevation = if (isDarkMode) 8.dp else 2.dp,
shape = RoundedCornerShape(21.dp),
spotColor = if (isDarkMode) Color.Black.copy(alpha = 0.5f) else Color.Black.copy(alpha = 0.1f)
)
.clip(RoundedCornerShape(21.dp))
.background(cardBackgroundColor)
.clickable( .clickable(
indication = null, indication = null,
interactionSource = remember { MutableInteractionSource() } interactionSource = remember { MutableInteractionSource() }
) { ) {
onClick() onClick()
} }
.padding(horizontal = 16.dp, vertical = 12.dp), .padding(16.dp)
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) { ) {
// 直接把 MBTI 图标和文字放在灰色卡片内部,布局与星座保持一致 Row(
Image( modifier = Modifier.fillMaxWidth(),
painter = painterResource(id = getMbtiImageResId(mbti, isDarkMode)), verticalAlignment = Alignment.CenterVertically
contentDescription = mbti, ) {
modifier = Modifier.size(96.dp) Text(
) text = mbti,
fontSize = 17.sp,
fontWeight = FontWeight.Normal,
color = if (isSelected) appColors.main else appColors.text,
modifier = Modifier.weight(1f)
)
Spacer(modifier = Modifier.height(0.dp)) if (isSelected) {
Icon(
Text( imageVector = Icons.Default.Check,
text = mbti, contentDescription = "Selected",
fontSize = 14.sp, modifier = Modifier.size(20.dp),
fontWeight = FontWeight.Medium, tint = appColors.main
color = appColors.text, )
textAlign = TextAlign.Center, }
modifier = Modifier.offset(y = (-10).dp) }
)
} }
} }

View File

@@ -1,19 +0,0 @@
package com.aiosman.ravenow.ui.account
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
object MbtiSheetManager {
private val _visible = MutableStateFlow(false)
val visible: StateFlow<Boolean> = _visible.asStateFlow()
fun open() {
_visible.value = true
}
fun close() {
_visible.value = false
}
}

View File

@@ -28,7 +28,6 @@ import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.Surface
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@@ -48,7 +47,6 @@ import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
import androidx.compose.ui.input.nestedscroll.NestedScrollSource import androidx.compose.ui.input.nestedscroll.NestedScrollSource
import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.input.nestedscroll.nestedScroll
@@ -137,164 +135,171 @@ fun ZodiacSelectBottomSheet(
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
// 确保弹窗展开
LaunchedEffect(Unit) {
sheetState.expand()
}
// 监听状态变化,确保弹窗始终展开(防止拖拽关闭和滑动)
LaunchedEffect(sheetState.currentValue, sheetState.targetValue, sheetState.isVisible) {
// 如果弹窗被拖拽关闭或位置发生变化,立即重新展开
if (!sheetState.isVisible || sheetState.targetValue != androidx.compose.material3.SheetValue.Expanded) {
kotlinx.coroutines.delay(10) // 短暂延迟确保状态更新
sheetState.expand()
}
}
val statusBarPadding = WindowInsets.systemBars.asPaddingValues()
val configuration = LocalConfiguration.current
val screenHeight = configuration.screenHeightDp.dp
val offsetY = screenHeight * 0.07f - statusBarPadding.calculateTopPadding()
ModalBottomSheet( ModalBottomSheet(
onDismissRequest = onClose, onDismissRequest = onClose,
sheetState = sheetState, sheetState = sheetState,
// 对齐发布动态草稿箱样式:底层透明,内容区域自己绘制圆角和背景 containerColor = sheetBackgroundColor, // 根据主题自适应背景
containerColor = Color.Transparent, dragHandle = null
shape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp),
dragHandle = {}
) { ) {
Box( Box(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.fillMaxHeight() .fillMaxHeight(0.95f)
.padding(top = 8.dp) .offset(y = offsetY)
.padding(
start = 16.dp,
end = 16.dp,
bottom = 8.dp
)
) { ) {
Surface( Column(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.fillMaxHeight() .fillMaxHeight()
.clip(RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp)),
color = sheetBackgroundColor,
tonalElevation = 0.dp,
shadowElevation = 0.dp,
) { ) {
Column( // 头部 - 使用 Box 实现绝对居中布局
Box(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.fillMaxHeight() .height(48.dp),
.padding(horizontal = 16.dp, vertical = 8.dp) contentAlignment = Alignment.Center
) { ) {
// 头部 - 使用 Box 实现绝对居中布局(对齐草稿箱样式) val cancelButtonGradientColors = if (isDarkMode) {
Box( listOf(
Color(0xFF3A3A3C),
Color(0xFF2C2C2E)
)
} else {
listOf(
Color(0xFFFFFFFF),
Color(0xFFF8F8F8)
)
}
val cancelButtonContentColor = if (isDarkMode) Color(0xFFFFFFFF) else Color(0xFF404040)
// 左上角返回按钮 - 根据 Swift 代码样式,带淡灰色渐变背景
Row(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .align(Alignment.CenterStart)
.height(48.dp), .height(36.dp)
contentAlignment = Alignment.Center .clip(RoundedCornerShape(18.dp)) // 圆角 100.0 在 36dp 高度下接近完全圆角
.background(
brush = Brush.linearGradient(
colors = cancelButtonGradientColors
// 不指定 start 和 end默认从左上到右下
)
)
.noRippleClickable { onClose() }
.padding(horizontal = 8.dp), // 内部 padding 确保内容不贴边
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically
) { ) {
val cancelButtonGradientColors = if (isDarkMode) { // 左箭头图标
listOf( Image(
Color(0xFF3A3A3C), painter = painterResource(id = R.drawable.rider_pro_back_icon),
Color(0xFF2C2C2E) contentDescription = null,
) modifier = Modifier.size(17.dp),
} else { colorFilter = ColorFilter.tint(cancelButtonContentColor)
listOf( )
Color(0xFFFFFFFF),
Color(0xFFF8F8F8)
)
}
val cancelButtonContentColor = if (isDarkMode) Color(0xFFFFFFFF) else Color(0xFF404040)
// 左上角返回按钮:参考 iOS 设计,整体「箭头 + 取消」在 91x44 的按钮内居中
Box(
modifier = Modifier
.align(Alignment.CenterStart)
.width(91.dp)
.height(44.dp)
.clip(RoundedCornerShape(1000.dp))
.background(
brush = Brush.linearGradient(
colors = cancelButtonGradientColors
)
)
.noRippleClickable { onClose() },
contentAlignment = Alignment.Center
) {
Row(
modifier = Modifier
// 不再固定宽度,让内容自然占位,避免裁剪掉“消”字
.padding(horizontal = 12.dp, vertical = 4.dp),
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically
) {
Image(
painter = painterResource(id = R.drawable.rider_pro_back_icon),
contentDescription = null,
modifier = Modifier.size(17.dp),
colorFilter = ColorFilter.tint(cancelButtonContentColor)
)
Spacer(modifier = Modifier.width(4.dp))
Text(
text = "取消",
fontSize = 17.sp,
fontWeight = FontWeight.Medium,
color = cancelButtonContentColor,
textAlign = androidx.compose.ui.text.style.TextAlign.Center,
maxLines = 1,
overflow = TextOverflow.Clip
)
}
}
// "取消" 文字
Text( Text(
text = stringResource(R.string.choose_zodiac), text = "取消",
color = appColors.text, fontSize = 17.sp,
fontSize = 20.sp, fontWeight = FontWeight.Medium,
fontWeight = FontWeight.Bold, color = cancelButtonContentColor,
textAlign = androidx.compose.ui.text.style.TextAlign.Center textAlign = androidx.compose.ui.text.style.TextAlign.Center
) )
} }
Spacer(Modifier.height(12.dp)) // 中间标题 - 绝对居中
Text(
text = stringResource(R.string.choose_zodiac),
color = appColors.text,
fontSize = 20.sp,
fontWeight = FontWeight.Bold,
textAlign = androidx.compose.ui.text.style.TextAlign.Center
)
}
// 创建 NestedScrollConnection Spacer(Modifier.height(12.dp))
// 1. 不抢在列表前面消费事件,让 LazyVerticalGrid 正常滚动
// 2. 在列表滚动之后把剩余滚动吃掉,避免继续传递到 BottomSheet 去触发下拉关闭
val nestedScrollConnection = remember {
object : NestedScrollConnection {
override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
// 不在这里消费,先让 LazyVerticalGrid 自己处理滚动
return Offset.Zero
}
override fun onPostScroll(consumed: Offset, available: Offset, source: NestedScrollSource): Offset { // 创建 NestedScrollConnection 来阻止滚动事件向上传播到 ModalBottomSheet
// 列表滚动完之后,把剩余滚动(尤其是向下拖拽)全部吃掉,防止再传给 BottomSheet val nestedScrollConnection = remember {
return available object : NestedScrollConnection {
} override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
// 不消费任何事件,让 LazyVerticalGrid 先处理
return Offset.Zero
}
override suspend fun onPreFling(available: Velocity): Velocity { override fun onPostScroll(consumed: Offset, available: Offset, source: NestedScrollSource): Offset {
// 不抢在列表前面处理 fling让 LazyVerticalGrid 先做惯性滚动 // 消费 LazyVerticalGrid 处理后的剩余滚动事件,防止传递到 ModalBottomSheet
return Velocity.Zero return available
} }
override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity { override suspend fun onPreFling(available: Velocity): Velocity {
// 列表惯性滚动之后,把剩余的 fling 速度吃掉,避免带动 BottomSheet 下滑关闭 // 不消费惯性滚动,让 LazyVerticalGrid 先处理
return available return Velocity.Zero
} }
override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity {
// 消费 LazyVerticalGrid 处理后的剩余惯性滚动,防止传递到 ModalBottomSheet
return available
} }
} }
}
// 网格列表 - 2列(与草稿箱一样放在内容区域内部滚动) // 网格列表 - 2列
LazyVerticalGrid( LazyVerticalGrid(
columns = GridCells.Fixed(2), columns = GridCells.Fixed(2),
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.weight(1f) .weight(1f)
.nestedScroll(nestedScrollConnection), .nestedScroll(nestedScrollConnection),
contentPadding = PaddingValues( contentPadding = PaddingValues(
start = 8.dp, start = 8.dp,
top = 8.dp, top = 8.dp,
end = 8.dp, end = 8.dp,
bottom = 8.dp bottom = 8.dp
), ),
horizontalArrangement = Arrangement.spacedBy(10.dp), horizontalArrangement = Arrangement.spacedBy(10.dp),
verticalArrangement = Arrangement.spacedBy(10.dp) verticalArrangement = Arrangement.spacedBy(10.dp)
) { ) {
itemsIndexed(ZODIAC_SIGN_RES_IDS) { index, zodiacResId -> itemsIndexed(ZODIAC_SIGN_RES_IDS) { index, zodiacResId ->
val zodiacText = stringResource(zodiacResId) val zodiacText = stringResource(zodiacResId)
ZodiacItem( ZodiacItem(
zodiac = zodiacText, zodiac = zodiacText,
zodiacResId = zodiacResId, zodiacResId = zodiacResId,
isSelected = zodiacResId == currentZodiacResId, isSelected = zodiacResId == currentZodiacResId,
onClick = { onClick = {
model.zodiac = zodiacText // 保存当前语言的星座文本
onClose() model.zodiac = zodiacText
// 立即保存到本地存储,确保选择后立即生效
AppState.UserId?.let { uid ->
com.aiosman.ravenow.AppStore.setUserZodiac(uid, zodiacText)
} }
) onClose()
} }
)
} }
} }
} }
@@ -335,9 +340,9 @@ fun ZodiacItem(
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.aspectRatio(1.1f) .aspectRatio(1.1f) // 增加宽高比,使高度相对更低
.shadow( .shadow(
elevation = if (isDarkMode) 8.dp else 2.dp, elevation = if (isDarkMode) 8.dp else 2.dp, // 深色模式下更强的阴影
shape = RoundedCornerShape(21.dp), shape = RoundedCornerShape(21.dp),
spotColor = if (isDarkMode) Color.Black.copy(alpha = 0.5f) else Color.Black.copy(alpha = 0.1f) spotColor = if (isDarkMode) Color.Black.copy(alpha = 0.5f) else Color.Black.copy(alpha = 0.1f)
) )
@@ -349,27 +354,30 @@ fun ZodiacItem(
) { ) {
onClick() onClick()
} }
.padding(horizontal = 16.dp, vertical = 12.dp), .padding(horizontal = 24.dp, vertical = 12.dp), // 减小垂直padding确保文本不被遮挡
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center verticalArrangement = Arrangement.Center
) { ) {
// 直接把图标和文字放在灰色卡片内部,不再额外嵌套一层 Box // 星座图标 - 使用对应星座的图片
Image( Box(
painter = painterResource(id = getZodiacImageResId(zodiacResId)), modifier = Modifier.size(100.dp),
contentDescription = zodiac, contentAlignment = Alignment.Center
// 图标稍微放大一些,让视觉更聚焦在星座图标上 ) {
modifier = Modifier.size(96.dp) Image(
) painter = painterResource(id = getZodiacImageResId(zodiacResId)),
contentDescription = zodiac,
Spacer(modifier = Modifier.height(0.dp)) modifier = Modifier.size(100.dp)
)
}
// 星座名称 - 使用负间距让文本向上移动,与图标更靠近
Text( Text(
text = zodiac, text = zodiac,
fontSize = 14.sp, fontSize = 14.sp,
fontWeight = FontWeight.Medium, fontWeight = FontWeight.Medium,
color = appColors.text, color = appColors.text,
textAlign = TextAlign.Center, textAlign = TextAlign.Center,
modifier = Modifier.offset(y = (-10).dp) // 再整体向上偏移 5dp共 10dp modifier = Modifier.offset(y = (-20).dp) // 负间距,让文本进一步向上移动
) )
} }
} }

View File

@@ -16,11 +16,9 @@ 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
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
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
import com.aiosman.ravenow.R import com.aiosman.ravenow.R
@@ -70,19 +68,6 @@ fun ChangePasswordScreen() {
var confirmPasswordError by remember { mutableStateOf<String?>(null) } var confirmPasswordError by remember { mutableStateOf<String?>(null) }
var passwordError by remember { mutableStateOf<String?>(null) } var passwordError by remember { mutableStateOf<String?>(null) }
val AppColors = LocalAppTheme.current val AppColors = LocalAppTheme.current
// 暗色模式下的 hint 文本颜色
val isDarkMode = AppState.darkMode
val hintColor = if (isDarkMode) {
Color(0xFFFFFFFF).copy(alpha = 0.7f)
} else {
null // 使用默认颜色
}
val labelColor = if (isDarkMode) {
Color(0xFFFFFFFF).copy(alpha = 0.7f)
} else {
null // 使用默认颜色
}
fun validate(): Boolean { fun validate(): Boolean {
// 使用通用密码校验器校验当前密码 // 使用通用密码校验器校验当前密码
val currentPasswordValidation = PasswordValidator.validateCurrentPassword(currentPassword, context) val currentPasswordValidation = PasswordValidator.validateCurrentPassword(currentPassword, context)
@@ -127,9 +112,7 @@ fun ChangePasswordScreen() {
password = true, password = true,
label = stringResource(R.string.current_password), label = stringResource(R.string.current_password),
hint = stringResource(R.string.current_password_tip5), hint = stringResource(R.string.current_password_tip5),
error = oldPasswordError, error = oldPasswordError
customHintColor = hintColor,
customLabelColor = labelColor
) )
Spacer(modifier = Modifier.height(4.dp)) Spacer(modifier = Modifier.height(4.dp))
TextInputField( TextInputField(
@@ -138,9 +121,7 @@ fun ChangePasswordScreen() {
password = true, password = true,
label = stringResource(R.string.new_password), label = stringResource(R.string.new_password),
hint = stringResource(R.string.new_password), hint = stringResource(R.string.new_password),
error = passwordError, error = passwordError
customHintColor = hintColor,
customLabelColor = labelColor
) )
Spacer(modifier = Modifier.height(4.dp)) Spacer(modifier = Modifier.height(4.dp))
TextInputField( TextInputField(
@@ -149,9 +130,7 @@ fun ChangePasswordScreen() {
password = true, password = true,
label = stringResource(R.string.confirm_new_password_tip1), label = stringResource(R.string.confirm_new_password_tip1),
hint = stringResource(R.string.new_password_tip1), hint = stringResource(R.string.new_password_tip1),
error = confirmPasswordError, error = confirmPasswordError
customHintColor = hintColor,
customLabelColor = labelColor
) )
Spacer(modifier = Modifier.height(50.dp)) Spacer(modifier = Modifier.height(50.dp))
ActionButton( ActionButton(

View File

@@ -70,8 +70,6 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.draw.shadow import androidx.compose.ui.draw.shadow
import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.text.TextLayoutResult
import com.aiosman.ravenow.ConstVars import com.aiosman.ravenow.ConstVars
import com.aiosman.ravenow.ui.composables.pickupAndCompressLauncher import com.aiosman.ravenow.ui.composables.pickupAndCompressLauncher
import android.widget.Toast import android.widget.Toast
@@ -79,8 +77,6 @@ import java.io.File
import androidx.activity.compose.BackHandler import androidx.activity.compose.BackHandler
import com.aiosman.ravenow.ui.account.ZodiacBottomSheetHost import com.aiosman.ravenow.ui.account.ZodiacBottomSheetHost
import com.aiosman.ravenow.ui.account.ZodiacSheetManager import com.aiosman.ravenow.ui.account.ZodiacSheetManager
import com.aiosman.ravenow.ui.account.MbtiBottomSheetHost
import com.aiosman.ravenow.ui.account.MbtiSheetManager
/** /**
* 编辑用户资料界面 * 编辑用户资料界面
@@ -198,8 +194,6 @@ fun AccountEditScreen2(onUpdateBanner: ((Uri, File, Context) -> Unit)? = null,)
) { ) {
// 挂载星座选择弹窗 // 挂载星座选择弹窗
ZodiacBottomSheetHost() ZodiacBottomSheetHost()
// 挂载MBTI选择弹窗
MbtiBottomSheetHost()
Box( Box(
modifier = Modifier modifier = Modifier
@@ -398,7 +392,7 @@ fun AccountEditScreen2(onUpdateBanner: ((Uri, File, Context) -> Unit)? = null,)
ProfileInfoCard( ProfileInfoCard(
label = stringResource(R.string.nickname), label = stringResource(R.string.nickname),
value = model.name, value = model.name,
placeholder = stringResource(R.string.nickname_placeholder), placeholder = "Value",
onValueChange = { onNicknameChange(it) }, onValueChange = { onNicknameChange(it) },
isMultiline = false isMultiline = false
) )
@@ -409,7 +403,7 @@ fun AccountEditScreen2(onUpdateBanner: ((Uri, File, Context) -> Unit)? = null,)
ProfileInfoCard( ProfileInfoCard(
label = stringResource(R.string.personal_intro), label = stringResource(R.string.personal_intro),
value = model.bio, value = model.bio,
placeholder = "", placeholder = "Welcome to my fantiac word i will show you something about magic",
onValueChange = { onBioChange(it) }, onValueChange = { onBioChange(it) },
isMultiline = true isMultiline = true
) )
@@ -431,7 +425,9 @@ fun AccountEditScreen2(onUpdateBanner: ((Uri, File, Context) -> Unit)? = null,)
iconResDark = null, // TODO: 添加MBTI暗色模式图标 iconResDark = null, // TODO: 添加MBTI暗色模式图标
iconResLight = null, // TODO: 添加MBTI亮色模式图标 iconResLight = null, // TODO: 添加MBTI亮色模式图标
onClick = { onClick = {
MbtiSheetManager.open() debouncedNavigation {
navController.navigate(NavigationRoute.MbtiSelect.route)
}
} }
) )
@@ -504,24 +500,12 @@ fun AccountEditScreen2(onUpdateBanner: ((Uri, File, Context) -> Unit)? = null,)
// 验证通过,执行保存 // 验证通过,执行保存
model.viewModelScope.launch { model.viewModelScope.launch {
model.isUpdating = true model.isUpdating = true
try { model.updateUserProfile(context)
model.updateUserProfile(context) model.viewModelScope.launch(Dispatchers.Main) {
model.viewModelScope.launch(Dispatchers.Main) { debouncedNavigation {
debouncedNavigation { navController.navigateUp()
navController.navigateUp()
}
model.isUpdating = false
}
} catch (e: Exception) {
// 捕获所有异常,包括网络异常
model.viewModelScope.launch(Dispatchers.Main) {
Toast.makeText(
context,
context.getString(R.string.network_error_check_network),
Toast.LENGTH_SHORT
).show()
model.isUpdating = false
} }
model.isUpdating = false
} }
} }
}, },
@@ -566,45 +550,29 @@ fun ProfileInfoCard(
isMultiline: Boolean = false isMultiline: Boolean = false
) { ) {
val appColors = LocalAppTheme.current val appColors = LocalAppTheme.current
var isFocused by remember { mutableStateOf(false) }
var lineCount by remember { mutableStateOf(1) }
// 根据行数决定对齐方式:单行时居中,多行时顶部对齐
val verticalAlignment = if (isMultiline) {
if (lineCount <= 1) Alignment.CenterVertically else Alignment.Top
} else {
Alignment.CenterVertically
}
Box( Box(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.height(if (isMultiline) 66.dp else 56.dp) // 昵称框高度56dp个人简介66dp .height(if (isMultiline) 66.dp else 56.dp) // 昵称框高度56dp个人简介66dp
.clip(RoundedCornerShape(16.dp)) .clip(RoundedCornerShape(16.dp))
.background(appColors.secondaryBackground), .background(appColors.secondaryBackground),
contentAlignment = if (isMultiline && lineCount > 1) Alignment.TopStart else Alignment.CenterStart contentAlignment = if (isMultiline) Alignment.TopStart else Alignment.CenterStart
) { ) {
Row( Row(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(horizontal = 16.dp) .padding(horizontal = 16.dp)
.padding(vertical = if (isMultiline && lineCount > 1) 11.dp else 0.dp), .padding(vertical = if (isMultiline) 11.dp else 0.dp),
verticalAlignment = verticalAlignment verticalAlignment = if (isMultiline) Alignment.Top else Alignment.CenterVertically
) { ) {
// 标签 // 标签
Box( Text(
modifier = Modifier text = label,
.width(100.dp) fontSize = 17.sp,
.height(if (isMultiline) 44.dp else 56.dp), fontWeight = FontWeight.Normal,
contentAlignment = Alignment.CenterStart color = appColors.text,
) { modifier = Modifier.width(100.dp)
Text( )
text = label,
fontSize = 17.sp,
fontWeight = FontWeight.Normal,
color = appColors.text
)
}
Spacer(modifier = Modifier.width(16.dp)) Spacer(modifier = Modifier.width(16.dp))
@@ -612,26 +580,10 @@ fun ProfileInfoCard(
Box( Box(
modifier = Modifier.weight(1f) modifier = Modifier.weight(1f)
) { ) {
// 对于个人简介isMultiline = true当值为空且没有焦点时显示图标 if (value.isEmpty()) {
if (value.isEmpty() && isMultiline && !isFocused) {
Box(
modifier = Modifier
.fillMaxWidth()
.height(44.dp),
contentAlignment = Alignment.CenterStart
) {
Icon(
painter = painterResource(id = R.mipmap.icons_infor_edit),
contentDescription = null,
modifier = Modifier.size(16.dp),
tint = appColors.secondaryText
)
}
} else if (value.isEmpty() && !isMultiline && placeholder.isNotEmpty()) {
// 对于非多行输入框,仍然显示 placeholder 文字
Text( Text(
text = placeholder, text = placeholder,
fontSize = 17.sp, fontSize = if (isMultiline) 15.sp else 17.sp,
fontWeight = FontWeight.Normal, fontWeight = FontWeight.Normal,
color = appColors.secondaryText, color = appColors.secondaryText,
modifier = Modifier.fillMaxWidth() modifier = Modifier.fillMaxWidth()
@@ -641,11 +593,7 @@ fun ProfileInfoCard(
BasicTextField( BasicTextField(
value = value, value = value,
onValueChange = onValueChange, onValueChange = onValueChange,
modifier = Modifier modifier = Modifier.fillMaxWidth(),
.fillMaxWidth()
.onFocusChanged { focusState ->
isFocused = focusState.isFocused
},
textStyle = androidx.compose.ui.text.TextStyle( textStyle = androidx.compose.ui.text.TextStyle(
fontSize = if (isMultiline) 15.sp else 17.sp, fontSize = if (isMultiline) 15.sp else 17.sp,
fontWeight = FontWeight.Normal, fontWeight = FontWeight.Normal,
@@ -653,12 +601,7 @@ fun ProfileInfoCard(
), ),
cursorBrush = SolidColor(appColors.text), cursorBrush = SolidColor(appColors.text),
maxLines = if (isMultiline) Int.MAX_VALUE else 1, maxLines = if (isMultiline) Int.MAX_VALUE else 1,
singleLine = !isMultiline, singleLine = !isMultiline
onTextLayout = { textLayoutResult: TextLayoutResult ->
if (isMultiline) {
lineCount = textLayoutResult.lineCount
}
}
) )
} }
} }

View File

@@ -17,7 +17,6 @@ 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
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
@@ -51,14 +50,6 @@ fun RemoveAccountScreen() {
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val context = LocalContext.current val context = LocalContext.current
// 暗色模式下的 hint 文本颜色
val isDarkMode = AppState.darkMode
val hintColor = if (isDarkMode) {
Color(0xFFFFFFFF).copy(alpha = 0.7f)
} else {
null // 使用默认颜色
}
fun removeAccount(password: String) { fun removeAccount(password: String) {
// 使用通用密码校验器 // 使用通用密码校验器
val passwordValidation = PasswordValidator.validateCurrentPassword(password, context) val passwordValidation = PasswordValidator.validateCurrentPassword(password, context)
@@ -141,8 +132,7 @@ fun RemoveAccountScreen() {
}, },
password = true, password = true,
hint = stringResource(R.string.remove_account_password_hint), hint = stringResource(R.string.remove_account_password_hint),
error = passwordError, error = passwordError
customHintColor = hintColor
) )
Spacer(modifier = Modifier.weight(1f)) Spacer(modifier = Modifier.weight(1f))

View File

@@ -16,6 +16,7 @@ import com.aiosman.ravenow.data.UserService
import com.aiosman.ravenow.data.UserServiceImpl import com.aiosman.ravenow.data.UserServiceImpl
import com.aiosman.ravenow.entity.AccountProfileEntity import com.aiosman.ravenow.entity.AccountProfileEntity
import com.aiosman.ravenow.entity.ChatItem import com.aiosman.ravenow.entity.ChatItem
import com.aiosman.ravenow.im.OpenIMManager
import io.openim.android.sdk.OpenIMClient import io.openim.android.sdk.OpenIMClient
import io.openim.android.sdk.enums.ConversationType import io.openim.android.sdk.enums.ConversationType
import io.openim.android.sdk.enums.ViewType import io.openim.android.sdk.enums.ViewType
@@ -100,6 +101,10 @@ abstract class BaseChatViewModel : ViewModel() {
override fun onSuccess(data: ConversationInfo) { override fun onSuccess(data: ConversationInfo) {
conversationID = data.conversationID conversationID = data.conversationID
// 如果是群组的会话id应该加上s修正,不知道是不是openIm的bug
if (data.conversationType == 2) {
conversationID = "s${conversationID}"
}
Log.d(getLogTag(), "获取会话信息成功conversationID: $conversationID") Log.d(getLogTag(), "获取会话信息成功conversationID: $conversationID")
onSuccess?.invoke() onSuccess?.invoke()
} }
@@ -324,6 +329,7 @@ abstract class BaseChatViewModel : ViewModel() {
OpenIMClient.getInstance().messageManager.getAdvancedHistoryMessageList( OpenIMClient.getInstance().messageManager.getAdvancedHistoryMessageList(
object : OnBase<AdvancedMessage> { object : OnBase<AdvancedMessage> {
override fun onSuccess(data: AdvancedMessage?) { override fun onSuccess(data: AdvancedMessage?) {
val messages = data?.messageList ?: emptyList() val messages = data?.messageList ?: emptyList()
val newChatItems = messages.mapNotNull { val newChatItems = messages.mapNotNull {
ChatItem.convertToChatItem(it, context, avatar = getMessageAvatar(it)) ChatItem.convertToChatItem(it, context, avatar = getMessageAvatar(it))

View File

@@ -504,6 +504,12 @@ fun ChatAiOtherItem(item: ChatItem) {
@Composable @Composable
fun ChatAiItem(item: ChatItem, currentUserId: String) { fun ChatAiItem(item: ChatItem, currentUserId: String) {
// 通知消息显示特殊布局
if (item.isNotification) {
NotificationMessageItem(item)
return
}
val isCurrentUser = item.userId == currentUserId val isCurrentUser = item.userId == currentUserId
if (isCurrentUser) { if (isCurrentUser) {
ChatAiSelfItem(item) ChatAiSelfItem(item)
@@ -617,7 +623,7 @@ fun ChatAiInput(
animationSpec = tween(300) animationSpec = tween(300)
) )
Image( Image(
painter = painterResource(R.mipmap.btn), painter = painterResource(R.mipmap.rider_pro_im_send),
modifier = Modifier modifier = Modifier
.size(24.dp) .size(24.dp)
.alpha(alpha) .alpha(alpha)

View File

@@ -516,6 +516,12 @@ fun ChatOtherItem(item: ChatItem) {
@Composable @Composable
fun ChatItem(item: ChatItem, currentUserId: String) { fun ChatItem(item: ChatItem, currentUserId: String) {
// 通知消息显示特殊布局
if (item.isNotification) {
NotificationMessageItem(item)
return
}
val isCurrentUser = item.userId == currentUserId val isCurrentUser = item.userId == currentUserId
if (isCurrentUser) { if (isCurrentUser) {
ChatSelfItem(item) ChatSelfItem(item)
@@ -650,7 +656,7 @@ fun ChatInput(
animationSpec = tween(300) animationSpec = tween(300)
) )
Image( Image(
painter = painterResource(R.mipmap.btn), painter = painterResource(R.mipmap.rider_pro_im_send),
modifier = Modifier modifier = Modifier
.size(24.dp) .size(24.dp)
.alpha(alpha) .alpha(alpha)

View File

@@ -183,21 +183,45 @@ fun GroupChatScreen(groupId: String,name: String,avatar: String,) {
) )
Spacer(modifier = Modifier.width(16.dp)) Spacer(modifier = Modifier.width(16.dp))
if (viewModel.groupInfo?.groupAvatar?.isNotEmpty() == true) { if (viewModel.groupAvatar.isNotEmpty()) {
CustomAsyncImage( CustomAsyncImage(
imageUrl = viewModel.groupInfo!!.groupAvatar, imageUrl = viewModel.groupAvatar,
modifier = Modifier modifier = Modifier
.size(35.dp) .size(32.dp)
.clip(RoundedCornerShape(15.dp)), .clip(RoundedCornerShape(8.dp))
.noRippleClickable {
navController.navigateToGroupInfo(groupId)
},
contentDescription = "群聊头像" contentDescription = "群聊头像"
) )
} else {
Box(
modifier = Modifier
.size(24.dp)
.clip(RoundedCornerShape(8.dp))
.background(AppColors.decentBackground)
.noRippleClickable {
navController.navigateToGroupInfo(groupId)
},
contentAlignment = Alignment.Center
) {
Text(
text = viewModel.groupName,
style = TextStyle(
color = AppColors.text,
fontSize = 18.sp,
fontWeight = androidx.compose.ui.text.font.FontWeight.W700
),
maxLines = 1,
overflow =TextOverflow.Ellipsis,
)
}
} }
Spacer(modifier = Modifier.width(8.dp)) Spacer(modifier = Modifier.width(8.dp))
Column( Column {
modifier = Modifier.weight(1f),
horizontalAlignment = Alignment.Start
) {
Text( Text(
text = viewModel.groupName, text = viewModel.groupName,
style = TextStyle( style = TextStyle(
@@ -205,21 +229,24 @@ fun GroupChatScreen(groupId: String,name: String,avatar: String,) {
fontSize = 18.sp, fontSize = 18.sp,
fontWeight = androidx.compose.ui.text.font.FontWeight.Bold fontWeight = androidx.compose.ui.text.font.FontWeight.Bold
), ),
maxLines = 1, maxLines = 1,
overflow = TextOverflow.Ellipsis overflow =TextOverflow.Ellipsis,
) )
} }
Image( Spacer(modifier = Modifier.weight(1f))
painter = painterResource(R.drawable.rider_pro_more_horizon), Box {
modifier = Modifier Image(
.size(28.dp) painter = painterResource(R.drawable.rider_pro_more_horizon),
.noRippleClickable { modifier = Modifier
navController.navigateToGroupInfo(groupId) .size(28.dp)
}, .noRippleClickable {
contentDescription = "更多", navController.navigateToGroupInfo(groupId)
colorFilter = ColorFilter.tint(AppColors.text) },
) contentDescription = null,
colorFilter = ColorFilter.tint(AppColors.text)
)
}
} }
} }
}, },
@@ -283,8 +310,13 @@ fun GroupChatScreen(groupId: String,name: String,avatar: String,) {
) )
} }
// 获取上一个item的userId用于判断是否显示头像和昵称 // 获取上一个item的userId用于判断是否显示头像和昵称
// 通知消息不参与判断逻辑
val previousItem = if (index < chatList.size - 1) chatList[index + 1] else null val previousItem = if (index < chatList.size - 1) chatList[index + 1] else null
val showAvatarAndNickname = previousItem?.userId != item.userId val showAvatarAndNickname = if (item.isNotification || previousItem?.isNotification == true) {
true // 通知消息前后都显示头像和昵称
} else {
previousItem?.userId != item.userId
}
GroupChatItem( GroupChatItem(
item = item, item = item,
currentUserId = viewModel.myProfile?.trtcUserId!!, currentUserId = viewModel.myProfile?.trtcUserId!!,
@@ -501,14 +533,14 @@ fun GroupChatOtherItem(item: ChatItem, showAvatarAndNickname: Boolean = true) {
@Composable @Composable
fun GroupChatItem(item: ChatItem, currentUserId: String, showAvatarAndNickname: Boolean = true) { fun GroupChatItem(item: ChatItem, currentUserId: String, showAvatarAndNickname: Boolean = true) {
val isCurrentUser = item.userId == currentUserId // 通知消息显示特殊布局(包括系统账户发送的消息)
if (item.isNotification) {
// 管理员消息显示特殊布局 NotificationMessageItem(item)
if (item.userId == "administrator") {
GroupChatAdminItem(item)
return return
} }
val isCurrentUser = item.userId == currentUserId
// 根据是否是当前用户显示不同样式 // 根据是否是当前用户显示不同样式
when (item.userId) { when (item.userId) {
currentUserId -> GroupChatSelfItem(item) currentUserId -> GroupChatSelfItem(item)
@@ -650,7 +682,7 @@ fun GroupChatInput(
animationSpec = tween(300) animationSpec = tween(300)
) )
Image( Image(
painter = painterResource(R.mipmap.btn), painter = painterResource(R.mipmap.rider_pro_im_send),
modifier = Modifier modifier = Modifier
.size(24.dp) .size(24.dp)
.alpha(alpha) .alpha(alpha)

View File

@@ -1,13 +1,11 @@
package com.aiosman.ravenow.ui.chat package com.aiosman.ravenow.ui.chat
import android.content.Context import android.content.Context
import android.util.Base64
import android.util.Log import android.util.Log
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.aiosman.ravenow.AppStore
import com.aiosman.ravenow.data.api.ApiClient import com.aiosman.ravenow.data.api.ApiClient
import com.aiosman.ravenow.data.api.SendChatAiRequestBody import com.aiosman.ravenow.data.api.SendChatAiRequestBody
import io.openim.android.sdk.enums.ConversationType import io.openim.android.sdk.enums.ConversationType
@@ -52,46 +50,17 @@ class GroupChatViewModel(
} }
private suspend fun getGroupInfo() { private suspend fun getGroupInfo() {
try { // 简化群组信息获取,使用默认信息
val response = ApiClient.api.getRoomDetail(trtcId = groupId) groupInfo = GroupInfo(
val room = response.body()?.data groupId = groupId,
groupInfo = room?.let { groupName = name,
GroupInfo( groupAvatar = avatar,
groupId = groupId, memberCount = 0,
groupName = it.name, ownerId = ""
groupAvatar = if (it.avatar.isNullOrEmpty()) { )
val groupIdBase64 = Base64.encodeToString( groupName = groupInfo?.groupName ?: ""
groupId.toByteArray(), groupAvatar = groupInfo?.groupAvatar ?: ""
Base64.NO_WRAP memberCount = groupInfo?.memberCount ?: 0
)
"${ApiClient.RETROFIT_URL}group/avatar?groupIdBase64=$groupIdBase64&token=${AppStore.token}"
} else {
"${ApiClient.BASE_API_URL}/outside${it.avatar}?token=${AppStore.token}"
},
memberCount = it.userCount,
ownerId = it.creator.userId
)
} ?: GroupInfo(
groupId = groupId,
groupName = name,
groupAvatar = avatar,
memberCount = 0,
ownerId = ""
)
} catch (e: Exception) {
Log.e("GroupChatViewModel", "加载群信息失败: ${e.message}", e)
groupInfo = GroupInfo(
groupId = groupId,
groupName = name,
groupAvatar = avatar,
memberCount = 0,
ownerId = ""
)
} finally {
groupName = groupInfo?.groupName ?: ""
groupAvatar = groupInfo?.groupAvatar ?: ""
memberCount = groupInfo?.memberCount ?: 0
}
} }
override fun getConversationParams(): Triple<String, Int, Boolean> { override fun getConversationParams(): Triple<String, Int, Boolean> {

View File

@@ -0,0 +1,58 @@
package com.aiosman.ravenow.ui.chat
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.aiosman.ravenow.LocalAppTheme
import com.aiosman.ravenow.entity.ChatItem
/**
* 通知消息显示组件
* 参考 iOS 的 tipsView 样式,用于显示通知类型的消息
*/
@Composable
fun NotificationMessageItem(item: ChatItem) {
val AppColors = LocalAppTheme.current
// 参考 iOS: HStack { Text(...) } .padding(.vertical, 8)
Row(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 8.dp),
horizontalArrangement = Arrangement.Center
) {
// 参考 iOS: Text(tips)
// .font(.caption2) - 12sp
// .foregroundColor(Color.textMain) - 主文本颜色
// .padding(.vertical, 8) .padding(.horizontal, 12)
// .background(Color.background.opacity(0.2))
// .background(.ultraThinMaterial.opacity(0.3))
// .cornerRadius(12)
Text(
text = item.message,
style = TextStyle(
color = AppColors.text, // 使用主文本颜色,不是次要文本颜色
fontSize = 12.sp, // .caption2 对应 12sp
textAlign = TextAlign.Center
),
modifier = Modifier
.clip(RoundedCornerShape(12.dp)) // 圆角 12不是 8
.background(
// 参考 iOS: Color.background.opacity(0.2) + .ultraThinMaterial.opacity(0.3)
// Android 使用半透明背景色模拟毛玻璃效果
AppColors.background.copy(alpha = 0.2f)
)
.padding(vertical = 8.dp, horizontal = 12.dp), // horizontal 12不是 16
maxLines = Int.MAX_VALUE
)
}
}

View File

@@ -15,9 +15,7 @@ import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.ime import androidx.compose.foundation.layout.ime
import androidx.compose.foundation.layout.navigationBars import androidx.compose.foundation.layout.navigationBars
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.HorizontalDivider
@@ -30,11 +28,9 @@ import androidx.compose.runtime.LaunchedEffect
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
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.platform.LocalSoftwareKeyboardController
@@ -42,7 +38,6 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
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 kotlinx.coroutines.launch
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
@@ -84,9 +79,8 @@ class CommentModalViewModel(
fun CommentModalContent( fun CommentModalContent(
postId: Int? = null, postId: Int? = null,
commentCount: Int = 0, commentCount: Int = 0,
onDismiss: () -> Unit = {}, onCommentAdded: () -> Unit = {},
showTitle: Boolean = true, onDismiss: () -> Unit = {}
onCommentAdded: () -> Unit = {}
) { ) {
val model = viewModel<CommentModalViewModel>( val model = viewModel<CommentModalViewModel>(
key = "CommentModalViewModel_$postId", key = "CommentModalViewModel_$postId",
@@ -110,8 +104,6 @@ fun CommentModalContent(
var softwareKeyboardController = LocalSoftwareKeyboardController.current var softwareKeyboardController = LocalSoftwareKeyboardController.current
var replyComment by remember { mutableStateOf<CommentEntity?>(null) } var replyComment by remember { mutableStateOf<CommentEntity?>(null) }
var shouldAutoFocus by remember { mutableStateOf(false) } var shouldAutoFocus by remember { mutableStateOf(false) }
val listState = rememberLazyListState()
val scope = rememberCoroutineScope()
LaunchedEffect(imePadding) { LaunchedEffect(imePadding) {
bottomPadding = imePadding.dp bottomPadding = imePadding.dp
@@ -169,42 +161,28 @@ fun CommentModalContent(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
) { ) {
// 拖动手柄
Box( Box(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(top = 8.dp, bottom = 12.dp), .padding(start = 16.dp, bottom = 16.dp, end = 16.dp)
contentAlignment = Alignment.Center
) { ) {
Box( Text(
modifier = Modifier stringResource(R.string.comment),
.width(40.dp) fontSize = 18.sp,
.height(4.dp) fontWeight = FontWeight.Bold,
.clip(RoundedCornerShape(50)) color = AppColors.text,
.background(AppColors.divider) modifier = Modifier.align(Alignment.Center)
) )
} }
if (showTitle) { HorizontalDivider(
Box( color = AppColors.divider
modifier = Modifier )
.fillMaxWidth()
.padding(start = 16.dp, bottom = 16.dp, end = 16.dp)
) {
Text(
stringResource(R.string.comment),
fontSize = 18.sp,
fontWeight = FontWeight.Bold,
color = AppColors.text,
modifier = Modifier.align(Alignment.Center)
)
}
}
Row( Row(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(horizontal = 20.dp, vertical = 8.dp), .padding(horizontal = 24.dp, vertical = 16.dp),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween horizontalArrangement = Arrangement.SpaceBetween
) { ) {
@@ -213,14 +191,9 @@ fun CommentModalContent(
fontSize = 14.sp, fontSize = 14.sp,
color = AppColors.secondaryText color = AppColors.secondaryText
) )
OrderSelectionComponent( OrderSelectionComponent {
selectedOrder = commentViewModel.order
) {
commentViewModel.order = it commentViewModel.order = it
commentViewModel.reloadComment() commentViewModel.reloadComment()
scope.launch {
listState.scrollToItem(0)
}
} }
} }
Box( Box(
@@ -231,8 +204,7 @@ fun CommentModalContent(
LazyColumn( LazyColumn(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.padding(horizontal = 16.dp), .padding(horizontal = 16.dp)
state = listState
) { ) {
item { item {
CommentContent( CommentContent(

View File

@@ -41,15 +41,16 @@ import com.aiosman.ravenow.R
import com.aiosman.ravenow.entity.CommentEntity import com.aiosman.ravenow.entity.CommentEntity
import com.aiosman.ravenow.exp.timeAgo import com.aiosman.ravenow.exp.timeAgo
import com.aiosman.ravenow.ui.NavigationRoute import com.aiosman.ravenow.ui.NavigationRoute
import com.aiosman.ravenow.ui.comment.NoticeScreenHeader
import com.aiosman.ravenow.ui.composables.CustomAsyncImage import com.aiosman.ravenow.ui.composables.CustomAsyncImage
import com.aiosman.ravenow.ui.composables.StatusBarMaskLayout import com.aiosman.ravenow.ui.composables.StatusBarSpacer
import com.aiosman.ravenow.ui.modifiers.noRippleClickable import com.aiosman.ravenow.ui.modifiers.noRippleClickable
import com.aiosman.ravenow.ui.navigateToPost import com.aiosman.ravenow.ui.navigateToPost
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import com.aiosman.ravenow.utils.NetworkUtils import com.aiosman.ravenow.utils.NetworkUtils
import com.aiosman.ravenow.ui.network.NetworkErrorContent import com.aiosman.ravenow.ui.network.ReloadButton
@Composable @Composable
fun CommentNoticeScreen(includeStatusBarPadding: Boolean = true){ fun CommentNoticeScreen() {
val viewModel = viewModel<CommentNoticeListViewModel>( val viewModel = viewModel<CommentNoticeListViewModel>(
key = "CommentNotice", key = "CommentNotice",
factory = object : ViewModelProvider.Factory { factory = object : ViewModelProvider.Factory {
@@ -67,22 +68,50 @@ fun CommentNoticeScreen(includeStatusBarPadding: Boolean = true){
val navController = LocalNavController.current val navController = LocalNavController.current
val AppColors = LocalAppTheme.current val AppColors = LocalAppTheme.current
StatusBarMaskLayout( Column(
modifier = Modifier modifier = Modifier.fillMaxSize().background(color = AppColors.background)
.background(color = AppColors.background)
.padding(horizontal = 16.dp),
darkIcons = !AppState.darkMode,
maskBoxBackgroundColor = AppColors.background,
includeStatusBarPadding = includeStatusBarPadding
) { ) {
StatusBarSpacer()
val isNetworkAvailable = NetworkUtils.isNetworkAvailable(LocalContext.current) val isNetworkAvailable = NetworkUtils.isNetworkAvailable(LocalContext.current)
if (!isNetworkAvailable) { if (!isNetworkAvailable) {
NetworkErrorContent( Box(
onReload = { modifier = Modifier
viewModel.initData(context, force = true) .fillMaxSize()
.padding(top = 149.dp),
contentAlignment = Alignment.TopCenter
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.fillMaxWidth()
) {
androidx.compose.foundation.Image(
painter = painterResource(id = R.mipmap.invalid_name_10),
contentDescription = "network error",
modifier = Modifier.size(181.dp)
)
Spacer(modifier = Modifier.size(24.dp))
Text(
text = stringResource(R.string.friend_chat_no_network_title),
color = AppColors.text,
fontSize = 16.sp,
fontWeight = FontWeight.W600
)
Spacer(modifier = Modifier.size(8.dp))
Text(
text = stringResource(R.string.friend_chat_no_network_subtitle),
color = AppColors.text,
fontSize = 14.sp,
fontWeight = FontWeight.W400
)
Spacer(modifier = Modifier.height(16.dp))
ReloadButton(
onClick = {
viewModel.initData(context, force = true)
}
)
} }
) }
} else if (comments.itemCount == 0 && comments.loadState.refresh is LoadState.NotLoading) { } else if (comments.itemCount == 0 && comments.loadState.refresh is LoadState.NotLoading) {
Box( Box(
modifier = Modifier modifier = Modifier
@@ -112,8 +141,7 @@ fun CommentNoticeScreen(includeStatusBarPadding: Boolean = true){
} else { } else {
LazyColumn( LazyColumn(
modifier = Modifier modifier = Modifier
.weight(1f) .fillMaxSize().padding(horizontal = 16.dp)
.background(color = AppColors.background)
) { ) {
items(comments.itemCount) { index -> items(comments.itemCount) { index ->
comments[index]?.let { comment -> comments[index]?.let { comment ->
@@ -187,58 +215,53 @@ fun CommentNoticeItem(
val navController = LocalNavController.current val navController = LocalNavController.current
val context = LocalContext.current val context = LocalContext.current
val AppColors = LocalAppTheme.current val AppColors = LocalAppTheme.current
val commentPrefix = stringResource(R.string.comment_notice)
Row( Row(
modifier = Modifier.padding(vertical = 12.dp) modifier = Modifier.padding(vertical = 20.dp, horizontal = 16.dp)
) { ) {
// 左侧头像区域 Box {
CustomAsyncImage( CustomAsyncImage(
context = context, context = context,
imageUrl = commentItem.avatar, imageUrl = commentItem.avatar,
contentDescription = commentItem.name, contentDescription = commentItem.name,
modifier = Modifier modifier = Modifier
.size(40.dp) .size(48.dp)
.clip(CircleShape) .clip(CircleShape)
.noRippleClickable { .noRippleClickable {
navController.navigate( navController.navigate(
NavigationRoute.AccountProfile.route.replace( NavigationRoute.AccountProfile.route.replace(
"{id}", "{id}",
commentItem.author.toString() commentItem.author.toString()
)
) )
) }
} )
) }
// 右侧内容区域
Row( Row(
modifier = Modifier modifier = Modifier
.weight(1f) .weight(1f)
.padding(start = 8.dp) .padding(start = 12.dp)
.noRippleClickable { .noRippleClickable {
onPostClick() onPostClick()
} }
) { ) {
// 主要信息列
Column( Column(
modifier = Modifier.weight(1f) modifier = Modifier.weight(1f)
) { ) {
Text( Text(
text = commentItem.name, text = commentItem.name,
fontSize = 14.sp, fontSize = 18.sp,
color = AppColors.text, modifier = Modifier,
maxLines = 1, color = AppColors.text
overflow = TextOverflow.Ellipsis
) )
Spacer(modifier = Modifier.height(4.dp)) Spacer(modifier = Modifier.height(4.dp))
// 评论内容行
Row { Row {
var text = commentItem.comment var text = commentItem.comment
if (commentItem.parentCommentId != null) { if (commentItem.parentCommentId != null) {
text = "Reply you: $text" text = "Reply you: $text"
} }
Text( Text(
text = "$commentPrefix $text", text = text,
fontSize = 14.sp, fontSize = 14.sp,
maxLines = 1, maxLines = 1,
color = AppColors.secondaryText, color = AppColors.secondaryText,
@@ -252,20 +275,25 @@ fun CommentNoticeItem(
color = AppColors.secondaryText, color = AppColors.secondaryText,
) )
} }
} }
Spacer(modifier = Modifier.width(4.dp)) Spacer(modifier = Modifier.width(24.dp))
// 右侧帖子图片
commentItem.post?.let { commentItem.post?.let {
Box { Box {
CustomAsyncImage( Box(
context = context, modifier = Modifier.padding(4.dp)
imageUrl = it.images[0].thumbnail, ) {
contentDescription = "Post Image", CustomAsyncImage(
modifier = Modifier context = context,
.size(40.dp) imageUrl = it.images[0].thumbnail,
.clip(RoundedCornerShape(8.dp)) contentDescription = "Post Image",
) modifier = Modifier
// 未读指示器 .size(48.dp).clip(RoundedCornerShape(8.dp))
)
// unread indicator
}
if (commentItem.unread) { if (commentItem.unread) {
Box( Box(
modifier = Modifier modifier = Modifier
@@ -275,7 +303,11 @@ fun CommentNoticeItem(
) )
} }
} }
} }
} }
} }
} }

View File

@@ -36,7 +36,6 @@ fun StatusBarMaskLayout(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
darkIcons: Boolean = true, darkIcons: Boolean = true,
useNavigationBarMask: Boolean = true, useNavigationBarMask: Boolean = true,
includeStatusBarPadding: Boolean = true,
maskBoxBackgroundColor: Color = Color.Transparent, maskBoxBackgroundColor: Color = Color.Transparent,
content: @Composable ColumnScope.() -> Unit content: @Composable ColumnScope.() -> Unit
) { ) {
@@ -51,13 +50,13 @@ fun StatusBarMaskLayout(
Column( Column(
modifier = modifier.fillMaxSize() modifier = modifier.fillMaxSize()
) { ) {
if (includeStatusBarPadding) { Box(
Box( modifier = Modifier
modifier = Modifier .height(paddingValues.calculateTopPadding())
.height(paddingValues.calculateTopPadding()) .fillMaxWidth()
.fillMaxWidth() .background(maskBoxBackgroundColor)
.background(maskBoxBackgroundColor) ) {
)
} }
content() content()
if (navigationBarPaddings > 24.dp && useNavigationBarMask) { if (navigationBarPaddings > 24.dp && useNavigationBarMask) {

View File

@@ -17,19 +17,15 @@ import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.PlayArrow
import androidx.compose.material.pullrefresh.PullRefreshIndicator 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.Icon
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
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
@@ -46,7 +42,7 @@ import com.aiosman.ravenow.ui.composables.StatusBarSpacer
import com.aiosman.ravenow.ui.favourite.FavouriteListViewModel.refreshPager import com.aiosman.ravenow.ui.favourite.FavouriteListViewModel.refreshPager
import com.aiosman.ravenow.ui.modifiers.noRippleClickable import com.aiosman.ravenow.ui.modifiers.noRippleClickable
import com.aiosman.ravenow.ui.navigateToPost import com.aiosman.ravenow.ui.navigateToPost
import com.aiosman.ravenow.ui.network.NetworkErrorContent import com.aiosman.ravenow.ui.network.ReloadButton
import com.aiosman.ravenow.utils.NetworkUtils import com.aiosman.ravenow.utils.NetworkUtils
@OptIn(ExperimentalMaterialApi::class) @OptIn(ExperimentalMaterialApi::class)
@@ -88,11 +84,43 @@ fun FavouriteListPage() {
var moments = dataFlow.collectAsLazyPagingItems() var moments = dataFlow.collectAsLazyPagingItems()
if (!isNetworkAvailable) { if (!isNetworkAvailable) {
NetworkErrorContent( Box(
onReload = { modifier = Modifier
model.refreshPager(force = true) .fillMaxSize()
.padding(top=149.dp),
contentAlignment = Alignment.TopCenter
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.fillMaxWidth()
) {
Image(
painter = painterResource(id = R.mipmap.invalid_name_10),
contentDescription = "network error",
modifier = Modifier.size(181.dp)
)
Spacer(modifier = Modifier.size(24.dp))
Text(
text = stringResource(R.string.friend_chat_no_network_title),
color = AppColors.text,
fontSize = 16.sp,
fontWeight = FontWeight.W600
)
Spacer(modifier = Modifier.size(8.dp))
Text(
text = stringResource(R.string.friend_chat_no_network_subtitle),
color = AppColors.secondaryText,
fontSize = 14.sp,
fontWeight = FontWeight.W400
)
Spacer(modifier = Modifier.height(16.dp))
ReloadButton(
onClick = {
model.refreshPager(force = true)
}
)
} }
) }
} else if(moments.itemCount == 0) { } else if(moments.itemCount == 0) {
Box( Box(
modifier = Modifier modifier = Modifier
@@ -159,10 +187,7 @@ fun FavouriteListPage() {
.clip(RoundedCornerShape(8.dp)), .clip(RoundedCornerShape(8.dp)),
context = context context = context
) )
if (momentItem.images.size > 1) {
val isVideoMoment = momentItem.images.isEmpty() && !momentItem.videos.isNullOrEmpty()
if (momentItem.images.size > 1 || (momentItem.videos?.size ?: 0) > 1) {
Box( Box(
modifier = Modifier modifier = Modifier
.padding(top = 8.dp, end = 8.dp) .padding(top = 8.dp, end = 8.dp)
@@ -175,31 +200,6 @@ fun FavouriteListPage() {
) )
} }
} }
if (isVideoMoment) {
Box(
modifier = Modifier
.padding(top = 8.dp, end = 8.dp)
.align(Alignment.TopEnd)
) {
Box(
modifier = Modifier
.size(24.dp)
.background(
color = Color.Black.copy(alpha = 0.4f),
shape = RoundedCornerShape(12.dp)
),
contentAlignment = Alignment.Center
) {
Icon(
imageVector = Icons.Default.PlayArrow,
contentDescription = "",
tint = Color.White,
modifier = Modifier.size(16.dp)
)
}
}
}
} }
} }
} }

View File

@@ -42,7 +42,7 @@ import com.aiosman.ravenow.ui.comment.NoticeScreenHeader
import com.aiosman.ravenow.ui.composables.StatusBarMaskLayout import com.aiosman.ravenow.ui.composables.StatusBarMaskLayout
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import com.aiosman.ravenow.utils.NetworkUtils import com.aiosman.ravenow.utils.NetworkUtils
import com.aiosman.ravenow.ui.network.NetworkErrorContent import com.aiosman.ravenow.ui.network.ReloadButton
@OptIn(ExperimentalMaterialApi::class) @OptIn(ExperimentalMaterialApi::class)
@Composable @Composable
@@ -76,11 +76,43 @@ fun FollowerListScreen(userId: Int) {
val isNetworkAvailable = NetworkUtils.isNetworkAvailable(LocalContext.current) val isNetworkAvailable = NetworkUtils.isNetworkAvailable(LocalContext.current)
if (!isNetworkAvailable) { if (!isNetworkAvailable) {
NetworkErrorContent( Box(
onReload = { modifier = Modifier
model.loadData(userId, true) .fillMaxSize()
.padding(top = 149.dp),
contentAlignment = Alignment.TopCenter
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.fillMaxWidth()
) {
Image(
painter = painterResource(id = R.mipmap.invalid_name_10),
contentDescription = "network error",
modifier = Modifier.size(181.dp)
)
Spacer(modifier = Modifier.size(24.dp))
androidx.compose.material.Text(
text = stringResource(R.string.friend_chat_no_network_title),
color = appColors.text,
fontSize = 16.sp,
fontWeight = FontWeight.W600
)
Spacer(modifier = Modifier.size(8.dp))
androidx.compose.material.Text(
text = stringResource(R.string.friend_chat_no_network_subtitle),
color = appColors.text,
fontSize = 14.sp,
fontWeight = FontWeight.W400
)
Spacer(modifier = Modifier.height(16.dp))
ReloadButton(
onClick = {
model.loadData(userId, true)
}
)
} }
) }
} else if (users.itemCount == 0) { } else if (users.itemCount == 0) {
Box( Box(
modifier = Modifier modifier = Modifier

View File

@@ -21,7 +21,6 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
@@ -41,7 +40,7 @@ import com.aiosman.ravenow.ui.composables.CustomAsyncImage
import com.aiosman.ravenow.ui.composables.FollowButton import com.aiosman.ravenow.ui.composables.FollowButton
import com.aiosman.ravenow.ui.composables.StatusBarMaskLayout import com.aiosman.ravenow.ui.composables.StatusBarMaskLayout
import com.aiosman.ravenow.ui.modifiers.noRippleClickable import com.aiosman.ravenow.ui.modifiers.noRippleClickable
import com.aiosman.ravenow.ui.network.NetworkErrorContent import com.aiosman.ravenow.ui.network.ReloadButton
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import com.aiosman.ravenow.utils.NetworkUtils import com.aiosman.ravenow.utils.NetworkUtils
@@ -49,14 +48,13 @@ import com.aiosman.ravenow.utils.NetworkUtils
* 关注消息列表 * 关注消息列表
*/ */
@Composable @Composable
fun FollowerNoticeScreen(includeStatusBarPadding: Boolean = true) { fun FollowerNoticeScreen() {
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val AppColors = LocalAppTheme.current val AppColors = LocalAppTheme.current
StatusBarMaskLayout( StatusBarMaskLayout(
modifier = Modifier.background(color = AppColors.background).padding(horizontal = 16.dp), modifier = Modifier.background(color = AppColors.background).padding(horizontal = 16.dp),
darkIcons = !AppState.darkMode, darkIcons = !AppState.darkMode,
maskBoxBackgroundColor = AppColors.background, maskBoxBackgroundColor = AppColors.background
includeStatusBarPadding = includeStatusBarPadding
) { ) {
val model = FollowerNoticeViewModel val model = FollowerNoticeViewModel
var dataFlow = model.followerItemsFlow var dataFlow = model.followerItemsFlow
@@ -68,11 +66,43 @@ fun FollowerNoticeScreen(includeStatusBarPadding: Boolean = true) {
val isNetworkAvailable = NetworkUtils.isNetworkAvailable(LocalContext.current) val isNetworkAvailable = NetworkUtils.isNetworkAvailable(LocalContext.current)
if (!isNetworkAvailable) { if (!isNetworkAvailable) {
NetworkErrorContent( Box(
onReload = { modifier = Modifier
model.reload(force = true) .fillMaxSize()
.padding(top=149.dp),
contentAlignment = Alignment.TopCenter
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.fillMaxWidth()
) {
Image(
painter = painterResource(id = R.mipmap.invalid_name_10),
contentDescription = "network error",
modifier = Modifier.size(181.dp)
)
Spacer(modifier = Modifier.size(24.dp))
androidx.compose.material.Text(
text = stringResource(R.string.friend_chat_no_network_title),
color = AppColors.text,
fontSize = 16.sp,
fontWeight = FontWeight.W600
)
Spacer(modifier = Modifier.size(8.dp))
androidx.compose.material.Text(
text = stringResource(R.string.friend_chat_no_network_subtitle),
color = AppColors.text,
fontSize = 14.sp,
fontWeight = FontWeight.W400
)
Spacer(modifier = Modifier.height(16.dp))
ReloadButton(
onClick = {
model.reload(force = true)
}
)
} }
) }
} else if (followers.itemCount == 0) { } else if (followers.itemCount == 0) {
Box( Box(
modifier = Modifier modifier = Modifier
@@ -141,57 +171,35 @@ fun FollowItem(
val AppColors = LocalAppTheme.current val AppColors = LocalAppTheme.current
val context = LocalContext.current val context = LocalContext.current
val navController = LocalNavController.current val navController = LocalNavController.current
val followText = stringResource(R.string.followed_you) Box(
Row(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .padding(vertical = 16.dp)
.padding(vertical = 12.dp), .noRippleClickable {
verticalAlignment = Alignment.CenterVertically navController.navigate(
) { NavigationRoute.AccountProfile.route.replace(
// 左侧头像区域 "{id}",
CustomAsyncImage( userId.toString()
context = context,
imageUrl = avatar,
contentDescription = nickname,
modifier = Modifier
.size(40.dp)
.clip(CircleShape)
.noRippleClickable {
navController.navigate(
NavigationRoute.AccountProfile.route.replace(
"{id}",
userId.toString()
)
) )
} )
) }
// 右侧内容区域 ) {
Row( Row(
modifier = Modifier modifier = Modifier.fillMaxWidth(),
.weight(1f) verticalAlignment = Alignment.CenterVertically,
.padding(start = 8.dp),
verticalAlignment = Alignment.CenterVertically
) { ) {
CustomAsyncImage(
context = context,
imageUrl = avatar,
contentDescription = nickname,
modifier = Modifier
.size(40.dp)
.clip(CircleShape)
)
Spacer(modifier = Modifier.width(12.dp))
Column( Column(
modifier = Modifier.weight(1f) modifier = Modifier.weight(1f)
) { ) {
Text( Text(nickname, fontWeight = FontWeight.Bold, fontSize = 16.sp, color = AppColors.text)
text = nickname,
fontWeight = FontWeight.Bold,
fontSize = 14.sp,
color = AppColors.text,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
Spacer(modifier = Modifier.height(4.dp))
Text(
text = followText,
fontSize = 14.sp,
color = AppColors.secondaryText,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
} }
if (!isFollowing && userId != AppState.UserId) { if (!isFollowing && userId != AppState.UserId) {
FollowButton( FollowButton(

View File

@@ -40,7 +40,7 @@ import com.aiosman.ravenow.exp.viewModelFactory
import com.aiosman.ravenow.ui.NavigationRoute import com.aiosman.ravenow.ui.NavigationRoute
import com.aiosman.ravenow.ui.comment.NoticeScreenHeader import com.aiosman.ravenow.ui.comment.NoticeScreenHeader
import com.aiosman.ravenow.ui.composables.StatusBarMaskLayout import com.aiosman.ravenow.ui.composables.StatusBarMaskLayout
import com.aiosman.ravenow.ui.network.NetworkErrorContent import com.aiosman.ravenow.ui.network.ReloadButton
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import com.aiosman.ravenow.utils.NetworkUtils import com.aiosman.ravenow.utils.NetworkUtils
@@ -78,11 +78,43 @@ fun FollowingListScreen(userId: Int) {
val isNetworkAvailable = NetworkUtils.isNetworkAvailable(LocalContext.current) val isNetworkAvailable = NetworkUtils.isNetworkAvailable(LocalContext.current)
if (!isNetworkAvailable) { if (!isNetworkAvailable) {
NetworkErrorContent( Box(
onReload = { modifier = Modifier
model.loadData(userId, true) .fillMaxSize()
.padding(top=149.dp),
contentAlignment = Alignment.TopCenter
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.fillMaxWidth()
) {
Image(
painter = painterResource(id = R.mipmap.invalid_name_10),
contentDescription = "network error",
modifier = Modifier.size(181.dp)
)
Spacer(modifier = Modifier.size(24.dp))
androidx.compose.material.Text(
text = stringResource(R.string.friend_chat_no_network_title),
color = appColors.text,
fontSize = 16.sp,
fontWeight = FontWeight.W600
)
Spacer(modifier = Modifier.size(8.dp))
androidx.compose.material.Text(
text = stringResource(R.string.friend_chat_no_network_subtitle),
color = appColors.secondaryText,
fontSize = 14.sp,
fontWeight = FontWeight.W400
)
Spacer(modifier = Modifier.height(16.dp))
ReloadButton(
onClick = {
model.loadData(userId, true)
}
)
} }
) }
} else if(users.itemCount == 0) { } else if(users.itemCount == 0) {
Box( Box(
modifier = Modifier modifier = Modifier

View File

@@ -99,15 +99,6 @@ fun CreateGroupChatScreen() {
} }
val navigationBarPadding = WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() val navigationBarPadding = WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding()
var showSelectTipsDialog by remember { mutableStateOf(false) }
// 自动隐藏“请选择群成员并输入群名称”提示弹窗
LaunchedEffect(showSelectTipsDialog) {
if (showSelectTipsDialog) {
kotlinx.coroutines.delay(2000)
showSelectTipsDialog = false
}
}
// 获取费用和余额信息 // 获取费用和余额信息
val pointsRules by PointService.pointsRules.collectAsState(initial = null) val pointsRules by PointService.pointsRules.collectAsState(initial = null)
@@ -504,134 +495,63 @@ fun CreateGroupChatScreen() {
} }
} }
// 创建群聊按钮 - 固定在底部(启用时使用渐变背景) // 创建群聊按钮 - 固定在底部
val isCreateEnabled = Button(
groupName.text.isNotEmpty() && selectedMembers.isNotEmpty() && !CreateGroupChatViewModel.isLoading onClick = {
// 创建群聊逻辑
Box( if (selectedMembers.isNotEmpty()) {
modifier = Modifier // 检查是否超过上限
.fillMaxWidth() if (selectedMembers.size > maxMemberLimit) {
.padding( CreateGroupChatViewModel.showError(context.getString(R.string.create_group_chat_exceed_limit, maxMemberLimit))
start = 16.dp, return@Button
end = 16.dp,
top = buttonTopPadding,
bottom = navigationBarPadding + 16.dp
)
.let { baseModifier ->
if (isCreateEnabled) {
baseModifier.background(
brush = Brush.linearGradient(
colors = listOf(
Color(0x997c45ed),
Color(0x997c68ef),
Color(0x997bd8f8)
)
),
shape = RoundedCornerShape(24.dp)
)
} else {
baseModifier
} }
} // 如果费用大于0显示确认弹窗
) { if (cost > 0) {
Button( CreateGroupChatViewModel.showConfirmDialog()
onClick = { } else {
// 创建群聊逻辑 // 费用为0直接创建
if (selectedMembers.isNotEmpty()) { scope.launch {
// 检查是否超过上限 val success = CreateGroupChatViewModel.createGroupChat(
if (selectedMembers.size > maxMemberLimit) { groupName = groupName.text,
CreateGroupChatViewModel.showError( selectedMembers = selectedMembers,
context.getString( context = context
R.string.create_group_chat_exceed_limit,
maxMemberLimit
)
) )
return@Button if (success) {
} navController.popBackStack()
// 如果费用大于0显示确认弹窗
if (cost > 0) {
CreateGroupChatViewModel.showConfirmDialog()
} else {
// 费用为0直接创建
scope.launch {
val success = CreateGroupChatViewModel.createGroupChat(
groupName = groupName.text,
selectedMembers = selectedMembers,
context = context
)
if (success) {
navController.popBackStack()
}
} }
} }
} }
},
modifier = Modifier
.fillMaxWidth(),
colors = ButtonDefaults.buttonColors(
containerColor = if (isCreateEnabled) Color.Transparent else AppColors.main,
contentColor = AppColors.mainText,
disabledContainerColor = AppColors.disabledBackground,
disabledContentColor = AppColors.text
),
shape = RoundedCornerShape(24.dp),
enabled = isCreateEnabled
) {
if (CreateGroupChatViewModel.isLoading) {
Text(
text = stringResource(R.string.agent_createing),
fontSize = 16.sp,
fontWeight = FontWeight.W600
)
} else {
Text(
text = stringResource(R.string.create_group_chat),
fontSize = 16.sp,
fontWeight = FontWeight.W600
)
} }
} },
modifier = Modifier
// 禁用状态下拦截点击并弹出提示 .fillMaxWidth()
if (!isCreateEnabled) { .padding(start = 16.dp, end = 16.dp, top = buttonTopPadding, bottom = navigationBarPadding + 16.dp),
Box( colors = ButtonDefaults.buttonColors(
modifier = Modifier containerColor = AppColors.main,
.matchParentSize() contentColor = AppColors.mainText,
.noRippleClickable { disabledContainerColor = AppColors.disabledBackground,
showSelectTipsDialog = true disabledContentColor = AppColors.text
} ),
shape = RoundedCornerShape(24.dp),
enabled = groupName.text.isNotEmpty() && selectedMembers.isNotEmpty() && !CreateGroupChatViewModel.isLoading
) {
if (CreateGroupChatViewModel.isLoading) {
Text(
text = stringResource(R.string.agent_createing),
fontSize = 16.sp,
fontWeight = FontWeight.W600
)
} else {
Text(
text = stringResource(R.string.create_group_chat),
fontSize = 16.sp,
fontWeight = FontWeight.W600
) )
} }
} }
} }
// 请选择群成员并输入群名称 提示弹窗
if (showSelectTipsDialog) {
Dialog(
onDismissRequest = { showSelectTipsDialog = false },
properties = DialogProperties(dismissOnClickOutside = true, dismissOnBackPress = true)
) {
Box(
modifier = Modifier
.background(color = AppColors.background, shape = RoundedCornerShape(16.dp))
.padding(horizontal = 20.dp, vertical = 16.dp)
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = stringResource(R.string.create_group_chat_select_members_and_name),
fontSize = 16.sp,
fontWeight = FontWeight.W600,
color = Color(0xFF7C45ED),
textAlign = TextAlign.Center
)
}
}
}
}
// 消费确认弹窗 // 消费确认弹窗
if (CreateGroupChatViewModel.showConfirmDialog) { if (CreateGroupChatViewModel.showConfirmDialog) {
CreateGroupChatConfirmDialog( CreateGroupChatConfirmDialog(

View File

@@ -2,7 +2,6 @@ package com.aiosman.ravenow.ui.group
import androidx.compose.foundation.Image import androidx.compose.foundation.Image
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
@@ -45,7 +44,6 @@ import com.aiosman.ravenow.ui.composables.PointsPaymentDialog
import com.aiosman.ravenow.ui.composables.StatusBarSpacer import com.aiosman.ravenow.ui.composables.StatusBarSpacer
import com.aiosman.ravenow.ui.index.NavItem import com.aiosman.ravenow.ui.index.NavItem
import com.aiosman.ravenow.ui.modifiers.noRippleClickable import com.aiosman.ravenow.ui.modifiers.noRippleClickable
import com.aiosman.ravenow.ui.navigateToAddGroupMember
import com.aiosman.ravenow.ui.navigateToGroupMembers import com.aiosman.ravenow.ui.navigateToGroupMembers
import com.aiosman.ravenow.ui.navigateToGroupProfileSettings import com.aiosman.ravenow.ui.navigateToGroupProfileSettings
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@@ -175,7 +173,7 @@ fun GroupChatInfoScreen(groupId: String) {
) )
Spacer(modifier = Modifier.width(4.dp)) Spacer(modifier = Modifier.width(4.dp))
Text( Text(
text = "${viewModel.groupInfo?.memberCount ?: 0}${stringResource(R.string.people)}", text = "${viewModel.groupInfo?.memberCount ?: 0}",
style = androidx.compose.ui.text.TextStyle( style = androidx.compose.ui.text.TextStyle(
color = AppColors.text.copy(alpha = 0.7f), color = AppColors.text.copy(alpha = 0.7f),
fontSize = 11.sp fontSize = 11.sp
@@ -189,42 +187,38 @@ fun GroupChatInfoScreen(groupId: String) {
item { item {
Spacer(modifier = Modifier.height(10.dp)) Spacer(modifier = Modifier.height(10.dp))
Row( Row(
modifier = Modifier modifier = Modifier.fillMaxWidth(),
.padding(horizontal = 24.dp)
.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceEvenly horizontalArrangement = Arrangement.SpaceEvenly
) { ) {
if (viewModel.groupInfo?.isCreator == true) { // 添加其他人
// 添加其他人 Column(
Column( horizontalAlignment = Alignment.CenterHorizontally,
horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier.noRippleClickable {
modifier = Modifier.noRippleClickable { // TODO: 实现添加其他人功能
navController.navigateToAddGroupMember(groupId, viewModel.groupInfo?.groupName) }
} ) {
Box(
modifier = Modifier
.size(30.dp)
.clip(CircleShape),
contentAlignment = Alignment.Center
) { ) {
Box( Image(
modifier = Modifier painter = painterResource(R.drawable.rider_pro_add_other),
.size(30.dp) modifier = Modifier.size(20.dp),
.clip(CircleShape), contentDescription = null,
contentAlignment = Alignment.Center colorFilter = ColorFilter.tint(
) { AppColors.text)
Image(
painter = painterResource(R.drawable.rider_pro_add_other),
modifier = Modifier.size(20.dp),
contentDescription = null,
colorFilter = ColorFilter.tint(
AppColors.text)
)
}
Spacer(modifier = Modifier.height(5.dp))
Text(
text = stringResource(R.string.group_chat_info_add_member),
style = androidx.compose.ui.text.TextStyle(
color = AppColors.text,
fontSize = 11.sp
)
) )
} }
Spacer(modifier = Modifier.height(5.dp))
Text(
text = stringResource(R.string.group_chat_info_add_member),
style = androidx.compose.ui.text.TextStyle(
color = AppColors.text,
fontSize = 11.sp
)
)
} }
// 通知设置 // 通知设置
@@ -264,11 +258,11 @@ fun GroupChatInfoScreen(groupId: String) {
) )
} }
// 分享群聊 // 退出群聊
Column( Column(
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.noRippleClickable { modifier = Modifier.noRippleClickable {
// TODO: 实现分享功能 // TODO: 实现退出群聊功能
} }
) { ) {
Box( Box(
@@ -278,7 +272,7 @@ fun GroupChatInfoScreen(groupId: String) {
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ) {
Image( Image(
painter = painterResource(R.mipmap.icon_share), painter = painterResource(R.drawable.group_info_edit),
modifier = Modifier.size(20.dp), modifier = Modifier.size(20.dp),
contentDescription = null, contentDescription = null,
colorFilter = ColorFilter.tint(AppColors.text) colorFilter = ColorFilter.tint(AppColors.text)
@@ -303,16 +297,16 @@ fun GroupChatInfoScreen(groupId: String) {
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.border(1.dp, AppColors.decentBackground, RoundedCornerShape(12.dp))
.clip(RoundedCornerShape(12.dp)) .clip(RoundedCornerShape(12.dp))
.background(AppColors.background) .background(AppColors.decentBackground.copy(alpha = 0.28f))
.padding(12.dp) .padding(12.dp)
) { ) {
Row(verticalAlignment = Alignment.CenterVertically) { Row(verticalAlignment = Alignment.CenterVertically) {
Image( Image(
painter = painterResource(R.mipmap.icons_brain), painter = painterResource(R.drawable.group_info_edit),
modifier = Modifier.size(20.dp), modifier = Modifier.size(16.dp),
contentDescription = null, contentDescription = null,
colorFilter = ColorFilter.tint(AppColors.text)
) )
Spacer(modifier = Modifier.width(6.dp)) Spacer(modifier = Modifier.width(6.dp))
Column(modifier = Modifier.weight(1f)) { Column(modifier = Modifier.weight(1f)) {
@@ -321,6 +315,7 @@ fun GroupChatInfoScreen(groupId: String) {
style = androidx.compose.ui.text.TextStyle( style = androidx.compose.ui.text.TextStyle(
color = AppColors.text, color = AppColors.text,
fontSize = 15.sp, fontSize = 15.sp,
fontWeight = FontWeight.Bold
) )
) )
Spacer(modifier = Modifier.height(2.dp)) Spacer(modifier = Modifier.height(2.dp))
@@ -342,7 +337,7 @@ fun GroupChatInfoScreen(groupId: String) {
modifier = Modifier modifier = Modifier
.weight(1f) .weight(1f)
.clip(RoundedCornerShape(20.dp)) .clip(RoundedCornerShape(20.dp))
.background(AppColors.decentBackground) .background(AppColors.background)
.padding(vertical = 8.dp) .padding(vertical = 8.dp)
.noRippleClickable { .noRippleClickable {
showAddMemoryDialog = true showAddMemoryDialog = true
@@ -361,7 +356,7 @@ fun GroupChatInfoScreen(groupId: String) {
modifier = Modifier modifier = Modifier
.weight(1f) .weight(1f)
.clip(RoundedCornerShape(20.dp)) .clip(RoundedCornerShape(20.dp))
.background(AppColors.decentBackground) .background(AppColors.background)
.padding(vertical = 8.dp) .padding(vertical = 8.dp)
.noRippleClickable { .noRippleClickable {
showMemoryManageDialog = true showMemoryManageDialog = true
@@ -384,84 +379,81 @@ fun GroupChatInfoScreen(groupId: String) {
item { item {
Spacer(modifier = Modifier.height(13.dp)) Spacer(modifier = Modifier.height(13.dp))
// 仅当当前用户是群聊创建者时显示以下组件 // 群资料设置
if (viewModel.groupInfo?.isCreator == true) { Row(
// 群资料设置 modifier = Modifier
Row( .fillMaxWidth()
modifier = Modifier .clip(RoundedCornerShape(8.dp))
.fillMaxWidth() .padding(12.dp)
.clip(RoundedCornerShape(8.dp)) .noRippleClickable {
.padding(12.dp) navController.navigateToGroupProfileSettings(groupId)
.noRippleClickable { },
navController.navigateToGroupProfileSettings(groupId) verticalAlignment = Alignment.CenterVertically
}, ) {
verticalAlignment = Alignment.CenterVertically Image(
) { painter = painterResource(R.drawable.group_info_edit),
Image( modifier = Modifier.size(20.dp),
painter = painterResource(R.mipmap.fengm), contentDescription = null,
modifier = Modifier.size(20.dp), colorFilter = ColorFilter.tint(
contentDescription = null, AppColors.text)
colorFilter = ColorFilter.tint( )
AppColors.text) Spacer(modifier = Modifier.width(10.dp))
) Text(
Spacer(modifier = Modifier.width(10.dp)) text = stringResource(R.string.group_chat_info_group_settings),
Text( style = androidx.compose.ui.text.TextStyle(
text = stringResource(R.string.group_chat_info_group_settings), color = AppColors.text,
style = androidx.compose.ui.text.TextStyle( fontSize = 15.sp
color = AppColors.text, ),
fontSize = 15.sp modifier = Modifier.weight(1f)
), )
modifier = Modifier.weight(1f) Image(
) painter = painterResource(R.drawable.rave_now_nav_right),
Image( modifier = Modifier.size(16.dp),
painter = painterResource(R.drawable.rave_now_nav_right), contentDescription = null,
modifier = Modifier.size(16.dp), )
contentDescription = null, }
)
}
// 群可见性 // 群可见性
Row( Row(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.clip(RoundedCornerShape(8.dp)) .clip(RoundedCornerShape(8.dp))
.padding(12.dp) .padding(12.dp)
.noRippleClickable { .noRippleClickable {
showVisibilityDialog = true showVisibilityDialog = true
}, },
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
Image( Image(
painter = painterResource(R.mipmap.rider_pro_change_password), painter = painterResource(R.mipmap.rider_pro_change_password),
modifier = Modifier.size(20.dp), modifier = Modifier.size(20.dp),
contentDescription = null, contentDescription = null,
colorFilter = ColorFilter.tint(AppColors.text) colorFilter = ColorFilter.tint(AppColors.text)
) )
Spacer(modifier = Modifier.width(10.dp)) Spacer(modifier = Modifier.width(10.dp))
Text( Text(
text = stringResource(R.string.group_chat_info_group_visibility), text = stringResource(R.string.group_chat_info_group_visibility),
style = androidx.compose.ui.text.TextStyle( style = androidx.compose.ui.text.TextStyle(
color = AppColors.text, color = AppColors.text,
fontSize = 15.sp fontSize = 15.sp
), ),
modifier = Modifier.weight(1f) modifier = Modifier.weight(1f)
) )
// 未解锁时才显示"待解锁" // 未解锁时才显示"待解锁"
if (viewModel.groupInfo?.privateFeePaid != true) { if (viewModel.groupInfo?.privateFeePaid != true) {
Text( Text(
text = stringResource(R.string.group_chat_info_locked), text = stringResource(R.string.group_chat_info_locked),
style = androidx.compose.ui.text.TextStyle( style = androidx.compose.ui.text.TextStyle(
color = AppColors.text.copy(alpha = 0.5f), color = AppColors.text.copy(alpha = 0.5f),
fontSize = 11.sp fontSize = 11.sp
)
)
}
Image(
painter = painterResource(R.drawable.rave_now_nav_right),
modifier = Modifier.size(16.dp),
contentDescription = null,
) )
)
} }
Image(
painter = painterResource(R.drawable.rave_now_nav_right),
modifier = Modifier.size(16.dp),
contentDescription = null,
)
} }
// 成员管理 // 成员管理
@@ -476,21 +468,15 @@ fun GroupChatInfoScreen(groupId: String) {
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
Image( Image(
painter = painterResource(R.mipmap.icons_users), painter = painterResource(R.drawable.group_info_users),
modifier = Modifier.size(25.dp), modifier = Modifier.size(20.dp),
contentDescription = null, contentDescription = null,
colorFilter = ColorFilter.tint( colorFilter = ColorFilter.tint(
AppColors.text) AppColors.text)
) )
Spacer(modifier = Modifier.width(10.dp)) Spacer(modifier = Modifier.width(10.dp))
Text( Text(
text = stringResource( text = stringResource(R.string.group_chat_info_member_manage),
if (viewModel.groupInfo?.isCreator == true) {
R.string.group_chat_info_member_manage
} else {
R.string.group_members_title
}
),
style = androidx.compose.ui.text.TextStyle( style = androidx.compose.ui.text.TextStyle(
color = AppColors.text, color = AppColors.text,
fontSize = 15.sp fontSize = 15.sp
@@ -513,8 +499,8 @@ fun GroupChatInfoScreen(groupId: String) {
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
Image( Image(
painter = painterResource(R.mipmap.iconsgallery), painter = painterResource(R.drawable.group_info_edit),
modifier = Modifier.size(25.dp), modifier = Modifier.size(20.dp),
contentDescription = null, contentDescription = null,
colorFilter = ColorFilter.tint( colorFilter = ColorFilter.tint(
AppColors.text) AppColors.text)
@@ -534,70 +520,36 @@ fun GroupChatInfoScreen(groupId: String) {
contentDescription = null, contentDescription = null,
) )
} }
if (viewModel.groupInfo?.isCreator == true) { // 解散群聊
// 解散群聊(仅群主) Row(
Row( modifier = Modifier
modifier = Modifier .fillMaxWidth()
.fillMaxWidth() .clip(RoundedCornerShape(8.dp))
.clip(RoundedCornerShape(8.dp)) .padding(12.dp)
.padding(12.dp) .noRippleClickable { },
.noRippleClickable { }, verticalAlignment = Alignment.CenterVertically
verticalAlignment = Alignment.CenterVertically ) {
) { Image(
Spacer(modifier = Modifier.width(3.dp)) painter = painterResource(R.drawable.group_info_exit),
Image( modifier = Modifier.size(20.dp),
painter = painterResource(R.mipmap.iconslogout), contentDescription = null,
modifier = Modifier.size(20.dp), colorFilter = ColorFilter.tint(Color(0xFFFF3B30))
contentDescription = null, )
colorFilter = ColorFilter.tint(Color(0xFFFF3B30)) Spacer(modifier = Modifier.width(10.dp))
) Text(
Spacer(modifier = Modifier.width(11.dp)) text = stringResource(R.string.group_chat_info_dissolve),
Text( style = androidx.compose.ui.text.TextStyle(
text = stringResource(R.string.group_chat_info_dissolve), color = Color(0xFFFF3B30),
style = androidx.compose.ui.text.TextStyle( fontSize = 15.sp
color = Color(0xFFFF3B30), ),
fontSize = 15.sp modifier = Modifier.weight(1f)
), )
modifier = Modifier.weight(1f) Image(
) painter = painterResource(R.drawable.rave_now_nav_right),
Image( modifier = Modifier.size(18.dp),
painter = painterResource(R.drawable.rave_now_nav_right), contentDescription = null,
modifier = Modifier.size(18.dp), colorFilter = ColorFilter.tint(Color(0xFFFF3B30))
contentDescription = null )
)
}
} else {
// 退出群聊(非群主)
Row(
modifier = Modifier
.fillMaxWidth()
.clip(RoundedCornerShape(8.dp))
.padding(12.dp)
.noRippleClickable { },
verticalAlignment = Alignment.CenterVertically
) {
Spacer(modifier = Modifier.width(3.dp))
Image(
painter = painterResource(R.drawable.group_info_exit),
modifier = Modifier.size(20.dp),
contentDescription = null,
colorFilter = ColorFilter.tint(Color(0xFFFF3B30))
)
Spacer(modifier = Modifier.width(11.dp))
Text(
text = stringResource(R.string.group_chat_info_quit),
style = androidx.compose.ui.text.TextStyle(
color = Color(0xFFFF3B30),
fontSize = 15.sp
),
modifier = Modifier.weight(1f)
)
Image(
painter = painterResource(R.drawable.rave_now_nav_right),
modifier = Modifier.size(18.dp),
contentDescription = null
)
}
} }
} }
} }

View File

@@ -107,16 +107,16 @@ fun GroupMembersScreen(groupId: String) {
textAlign = TextAlign.Center textAlign = TextAlign.Center
) )
// androidx.compose.foundation.Image( androidx.compose.foundation.Image(
// painter = painterResource(R.drawable.rider_pro_add_other), painter = painterResource(R.drawable.rider_pro_add_other),
// contentDescription = stringResource(R.string.group_chat_info_add_member), contentDescription = stringResource(R.string.group_chat_info_add_member),
// colorFilter = ColorFilter.tint(AppColors.text), colorFilter = ColorFilter.tint(AppColors.text),
// modifier = Modifier modifier = Modifier
// .size(24.dp) .size(24.dp)
// .noRippleClickable { .noRippleClickable {
// navController.navigateToAddGroupMember(groupId, viewModel.groupInfo?.groupName) navController.navigateToAddGroupMember(groupId, viewModel.groupInfo?.groupName)
// } }
// ) )
} }
} }
@@ -391,7 +391,7 @@ private fun MemberItem(
Spacer(modifier = Modifier.weight(1f)) Spacer(modifier = Modifier.weight(1f))
// 菜单按钮 // 菜单按钮
if (isAdmin) {
IconButton( IconButton(
onClick = { onMenuClick(itemPosition, itemHeight) }, onClick = { onMenuClick(itemPosition, itemHeight) },
modifier = Modifier.size(24.dp) modifier = Modifier.size(24.dp)
@@ -404,7 +404,6 @@ private fun MemberItem(
.size(24.dp) .size(24.dp)
) )
} }
}
Spacer(modifier = Modifier.width(8.dp)) Spacer(modifier = Modifier.width(8.dp))

View File

@@ -2,6 +2,10 @@ package com.aiosman.ravenow.ui.index
import androidx.compose.animation.animateColorAsState import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.FastOutLinearInEasing
import androidx.compose.animation.core.LinearOutSlowInEasing
import androidx.compose.animation.core.updateTransition
import androidx.compose.animation.core.animateFloat
import androidx.compose.animation.core.FastOutSlowInEasing import androidx.compose.animation.core.FastOutSlowInEasing
import androidx.compose.animation.core.tween import androidx.compose.animation.core.tween
import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.ExperimentalFoundationApi
@@ -136,7 +140,6 @@ fun IndexScreen() {
ModalNavigationDrawer( ModalNavigationDrawer(
drawerState = drawerState, drawerState = drawerState,
gesturesEnabled = drawerState.isOpen, gesturesEnabled = drawerState.isOpen,
scrimColor = Color.Black.copy(alpha = 0.6f),
drawerContent = { drawerContent = {
CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Ltr) { CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Ltr) {
SideMenuContent( SideMenuContent(
@@ -522,6 +525,8 @@ fun SideMenuContent(
} else { } else {
Color(0xFFFAF9FB) // 亮色模式:浅灰色 Color(0xFFFAF9FB) // 亮色模式:浅灰色
} }
// 遮罩颜色 黑色透明度0.6
val overlayColor = Color.Black.copy(alpha = 0.6f)
// 卡片背景色 - 根据暗色模式适配 // 卡片背景色 - 根据暗色模式适配
val cardBackgroundColor = if (darkModeEnabled) { val cardBackgroundColor = if (darkModeEnabled) {
appColors.background // 暗色模式:深色背景 appColors.background // 暗色模式:深色背景
@@ -541,6 +546,24 @@ fun SideMenuContent(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
) { ) {
// 左侧半透明遮罩(平滑淡入淡出)
val overlayTransition = updateTransition(targetState = isDrawerOpen, label = "overlay")
val overlayAlpha by overlayTransition.animateFloat(
transitionSpec = {
if (targetState) {
tween(durationMillis = 400, easing = LinearOutSlowInEasing)
} else {
tween(durationMillis = 300, easing = FastOutLinearInEasing)
}
},
label = "overlayAlpha"
) { open -> if (open) 0.6f else 0f }
Box(
modifier = Modifier
.fillMaxSize()
.background(Color.Black.copy(alpha = overlayAlpha))
)
// 右侧菜单面板 // 右侧菜单面板
Box( Box(
modifier = Modifier modifier = Modifier

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) }
@@ -165,7 +137,7 @@ fun Agent() {
contentDescription = "Rave AI Logo", contentDescription = "Rave AI Logo",
modifier = Modifier modifier = Modifier
.height(44.dp) .height(44.dp)
.padding(top =9.dp,bottom=9.dp) .padding(top = 9.dp, bottom = 9.dp)
.wrapContentSize(), .wrapContentSize(),
// colorFilter = ColorFilter.tint(AppColors.text) // colorFilter = ColorFilter.tint(AppColors.text)
) )
@@ -176,7 +148,7 @@ fun Agent() {
contentDescription = "search", contentDescription = "search",
modifier = Modifier modifier = Modifier
.size(44.dp) .size(44.dp)
.padding(top = 9.dp,bottom=9.dp) .padding(top = 9.dp, bottom = 9.dp)
.noRippleClickable { .noRippleClickable {
navController.navigate(NavigationRoute.Search.route) navController.navigate(NavigationRoute.Search.route)
}, },
@@ -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,133 +309,81 @@ fun Agent() {
} }
// 只有当热门聊天室有数据时,才展示“发现更多”区域 // 只有当热门聊天室有数据时,才展示“发现更多”区域
if (viewModel.chatRooms.isNotEmpty()) { item { Spacer(modifier = Modifier.height(20.dp)) }
item { Spacer(modifier = Modifier.height(20.dp)) }
// "发现更多" 标题 - 吸顶 // "发现更多" 标题 - 吸顶
stickyHeader(key = "discover_more") { stickyHeader(key = "discover_more") {
Row( Row(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.background(AppColors.background) .background(AppColors.background)
.padding(top = 8.dp, bottom = 12.dp), .padding(top = 8.dp, bottom = 12.dp),
horizontalArrangement = Arrangement.Start, horizontalArrangement = Arrangement.Start,
verticalAlignment = Alignment.Bottom verticalAlignment = Alignment.Bottom
) { ) {
Image( Image(
painter = painterResource(R.mipmap.bars_x_buttons_home_n_copy_2), painter = painterResource(R.mipmap.bars_x_buttons_home_n_copy_2),
contentDescription = "agent", contentDescription = "agent",
modifier = Modifier.size(28.dp) modifier = Modifier.size(28.dp)
) )
Spacer(modifier = Modifier.width(4.dp)) Spacer(modifier = Modifier.width(4.dp))
androidx.compose.material3.Text( androidx.compose.material3.Text(
text = stringResource(R.string.agent_find), text = stringResource(R.string.agent_find),
fontSize = 16.sp, fontSize = 16.sp,
fontWeight = androidx.compose.ui.text.font.FontWeight.W900, fontWeight = androidx.compose.ui.text.font.FontWeight.W900,
color = AppColors.text color = AppColors.text
) )
}
} }
}
// Agent网格 - 使用行式布局 // Agent网格 - 使用行式布局
items( items(
items = agentItems.chunked(2), items = agentItems.chunked(2),
key = { row -> row.firstOrNull()?.openId ?: "" } key = { row -> row.firstOrNull()?.openId ?: "" }
) { rowItems -> ) { rowItems ->
Row( Row(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(vertical = 16.dp), .padding(vertical = 16.dp),
horizontalArrangement = Arrangement.spacedBy(16.dp) horizontalArrangement = Arrangement.spacedBy(16.dp)
) { ) {
rowItems.forEach { agentItem -> rowItems.forEach { agentItem ->
Box(
modifier = Modifier.weight(1f)
) {
AgentCardSquare(
agentItem = agentItem,
viewModel = viewModel,
navController = LocalNavController.current
)
}
}
// 如果这一行只有一个item添加一个空的占位符
if (rowItems.size == 1) {
Spacer(modifier = Modifier.weight(1f))
}
}
}
// 加载更多指示器(仅在展示"发现更多"时显示)
if (viewModel.isLoadingMore) {
item {
Box( Box(
modifier = Modifier modifier = Modifier.weight(1f)
.fillMaxWidth()
.padding(vertical = 24.dp),
contentAlignment = Alignment.Center
) { ) {
LottieAnimation( AgentCardSquare(
composition = rememberLottieComposition(LottieCompositionSpec.Asset("star_Loader.lottie")).value, agentItem = agentItem,
iterations = LottieConstants.IterateForever, viewModel = viewModel,
modifier = Modifier.size(80.dp) navController = LocalNavController.current
) )
} }
} }
// 如果这一行只有一个item添加一个空的占位符
if (rowItems.size == 1) {
Spacer(modifier = Modifier.weight(1f))
}
} }
} }
}
}
}
@Composable // 加载更多指示器(仅在展示"发现更多"时显示)
fun AgentGridLayout( if (viewModel.isLoadingMore) {
agentItems: List<AgentItem>, item {
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( Box(
modifier = Modifier.weight(1f) modifier = Modifier
.fillMaxWidth()
.padding(vertical = 24.dp),
contentAlignment = Alignment.Center
) { ) {
AgentCardSquare( LottieAnimation(
agentItem = rowItems[1], composition = rememberLottieComposition(LottieCompositionSpec.Asset("star_Loader.lottie")).value,
viewModel = viewModel, iterations = LottieConstants.IterateForever,
navController = navController modifier = Modifier.size(80.dp)
) )
} }
} else {
// 如果只有一列,添加空白占位
Spacer(modifier = Modifier.weight(1f))
} }
} }
} }
} }
} }
@@ -486,11 +414,11 @@ fun AgentCardSquare(
} }
) { ) {
// 背景大图 // 背景大图
CustomAsyncImage( CustomAsyncImage(
imageUrl = agentItem.avatar, imageUrl = agentItem.avatar,
contentDescription = agentItem.title, contentDescription = agentItem.title,
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
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
) )
@@ -507,27 +435,27 @@ fun AgentCardSquare(
) )
.padding(12.dp) .padding(12.dp)
) { ) {
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(bottom = 40.dp) // 为底部聊天按钮预留空间 .padding(bottom = 40.dp) // 为底部聊天按钮预留空间
) { ) {
androidx.compose.material3.Text( androidx.compose.material3.Text(
text = agentItem.title, text = agentItem.title,
color = Color.White, color = Color.White,
fontSize = 14.sp, fontSize = 14.sp,
fontWeight = androidx.compose.ui.text.font.FontWeight.W700, fontWeight = androidx.compose.ui.text.font.FontWeight.W700,
maxLines = 1, maxLines = 1,
overflow = TextOverflow.Ellipsis overflow = TextOverflow.Ellipsis
) )
Spacer(modifier = Modifier.height(4.dp)) Spacer(modifier = Modifier.height(4.dp))
androidx.compose.material3.Text( androidx.compose.material3.Text(
text = agentItem.desc, text = agentItem.desc,
color = Color.White.copy(alpha = 0.92f), color = Color.White.copy(alpha = 0.92f),
fontSize = 11.sp, fontSize = 11.sp,
maxLines = 2, maxLines = 2,
overflow = TextOverflow.Ellipsis overflow = TextOverflow.Ellipsis
) )
} }
} }
@@ -562,9 +490,10 @@ fun AgentCardSquare(
} }
} }
} }
@OptIn(ExperimentalFoundationApi::class) @OptIn(ExperimentalFoundationApi::class)
@Composable @Composable
fun AgentViewPagerSection(agentItems: List<AgentItem>,viewModel: AgentViewModel) { fun AgentViewPagerSection(agentItems: List<AgentItem>, viewModel: AgentViewModel) {
val AppColors = LocalAppTheme.current val AppColors = LocalAppTheme.current
if (agentItems.isEmpty()) return 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 @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(
topStart = 12.dp, RoundedCornerShape(
topEnd = 12.dp, topStart = 12.dp,
bottomStart = 0.dp, topEnd = 12.dp,
bottomEnd = 0.dp)), bottomStart = 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会处理数据加载
try { // 避免重复请求因为AiProfileViewModel.loadProfile已经支持通过openId加载
val profile = userService.getUserProfileByOpenId(openId) try {
// 从Agent列表点击进去的一定是智能体直接传递isAiAccount = true navController.navigate(
navController.navigate( NavigationRoute.AccountProfile.route
NavigationRoute.AccountProfile.route .replace("{id}", openId)
.replace("{id}", profile.id.toString()) .replace("{isAiAccount}", "true")
.replace("{isAiAccount}", "true") )
) } catch (e: Exception) {
} catch (e: Exception) { Log.e("AgentViewModel", "Navigation failed", e)
// swallow error to avoid crash on navigation attempt failures e.printStackTrace()
}
} }
} }

View File

@@ -10,7 +10,6 @@ import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize 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.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.width import androidx.compose.foundation.layout.width
@@ -49,12 +48,11 @@ import com.aiosman.ravenow.ui.composables.CustomAsyncImage
import com.aiosman.ravenow.ui.composables.rememberDebouncer import com.aiosman.ravenow.ui.composables.rememberDebouncer
import com.aiosman.ravenow.ui.modifiers.noRippleClickable import com.aiosman.ravenow.ui.modifiers.noRippleClickable
import com.aiosman.ravenow.utils.NetworkUtils import com.aiosman.ravenow.utils.NetworkUtils
import com.aiosman.ravenow.ui.network.NetworkErrorContentCompact import com.aiosman.ravenow.ui.network.ReloadButton
import com.airbnb.lottie.compose.LottieAnimation import com.airbnb.lottie.compose.LottieAnimation
import com.airbnb.lottie.compose.LottieCompositionSpec import com.airbnb.lottie.compose.LottieCompositionSpec
import com.airbnb.lottie.compose.LottieConstants import com.airbnb.lottie.compose.LottieConstants
import com.airbnb.lottie.compose.rememberLottieComposition import com.airbnb.lottie.compose.rememberLottieComposition
import com.aiosman.ravenow.ui.index.tabs.profile.composable.ChatEmptyStateView
/** /**
* 智能体聊天列表页面 * 智能体聊天列表页面
@@ -91,28 +89,72 @@ fun AgentChatListScreen() {
.pullRefresh(state) .pullRefresh(state)
) { ) {
if (AgentChatListViewModel.agentChatList.isEmpty() && !AgentChatListViewModel.isLoading) { if (AgentChatListViewModel.agentChatList.isEmpty() && !AgentChatListViewModel.isLoading) {
Box( // 空状态
Column(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.padding(horizontal = 16.dp), .padding(16.dp),
contentAlignment = Alignment.Center horizontalAlignment = Alignment.CenterHorizontally,
) {
// 空状态
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.offset(y = (-40).dp)
) {
val isNetworkAvailable = NetworkUtils.isNetworkAvailable(context)
if (isNetworkAvailable) { ) {
ChatEmptyStateView() val isNetworkAvailable = NetworkUtils.isNetworkAvailable(context)
} else {
NetworkErrorContentCompact( if (isNetworkAvailable) {
onReload = { Spacer(modifier = Modifier.height(39.dp))
AgentChatListViewModel.refreshPager(context = context) Image(
} painter = painterResource(id = R.mipmap.invalid_name_3),
) contentDescription = "null data",
} modifier = Modifier
.width(181.dp)
.height(153.dp)
)
Spacer(modifier = Modifier.height(9.dp))
Text(
text = stringResource(R.string.no_one_knocked_yet),
color = AppColors.text,
fontSize = 16.sp,
fontWeight = FontWeight.W600,
textAlign = TextAlign.Center,
modifier = Modifier.padding(horizontal = 24.dp),
maxLines = 2,
overflow = TextOverflow.Ellipsis
)
}
else {
Spacer(modifier = Modifier.height(39.dp))
Image(
painter = painterResource(id = R.mipmap.invalid_name_10),
contentDescription = "network error",
modifier = Modifier
.size(181.dp)
)
Spacer(modifier = Modifier.height(24.dp))
Text(
text = stringResource(R.string.friend_chat_no_network_title),
color = AppColors.text,
fontSize = 16.sp,
fontWeight = FontWeight.W600,
textAlign = TextAlign.Center,
modifier = Modifier.padding(horizontal = 24.dp),
maxLines = 2,
overflow = TextOverflow.Ellipsis
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = stringResource(R.string.friend_chat_no_network_subtitle),
color = AppColors.secondaryText,
fontSize = 14.sp,
textAlign = TextAlign.Center,
modifier = Modifier.padding(horizontal = 24.dp),
maxLines = 3,
overflow = TextOverflow.Ellipsis
)
Spacer(modifier = Modifier.height(16.dp))
ReloadButton(
onClick = {
AgentChatListViewModel.refreshPager(context = context)
}
)
} }
} }
} else { } else {

View File

@@ -42,11 +42,10 @@ import com.aiosman.ravenow.ui.composables.CustomAsyncImage
import com.aiosman.ravenow.ui.composables.rememberDebouncer import com.aiosman.ravenow.ui.composables.rememberDebouncer
import com.aiosman.ravenow.ui.modifiers.noRippleClickable import com.aiosman.ravenow.ui.modifiers.noRippleClickable
import com.aiosman.ravenow.utils.NetworkUtils import com.aiosman.ravenow.utils.NetworkUtils
import com.aiosman.ravenow.ui.network.NetworkErrorContentCompact import com.aiosman.ravenow.ui.network.ReloadButton
import androidx.compose.material.Button import androidx.compose.material.Button
import androidx.compose.material.ButtonDefaults import androidx.compose.material.ButtonDefaults
import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontFamily
import com.aiosman.ravenow.ui.index.tabs.profile.composable.ChatEmptyStateView
data class CombinedConversation( data class CombinedConversation(
val type: String, // "agent", "group", or "friend" val type: String, // "agent", "group", or "friend"
@@ -218,31 +217,73 @@ fun AllChatListScreen() {
.pullRefresh(state) .pullRefresh(state)
) { ) {
if (allConversations.isEmpty() && !isLoading) { if (allConversations.isEmpty() && !isLoading) {
Box( Column(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.padding(horizontal = 16.dp), .padding(16.dp),
contentAlignment = Alignment.Center horizontalAlignment = Alignment.CenterHorizontally,
) { ) {
Column( val isNetworkAvailable = NetworkUtils.isNetworkAvailable(context)
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.offset(y = (-40).dp)
) {
val isNetworkAvailable = NetworkUtils.isNetworkAvailable(context)
if (isNetworkAvailable) { if (isNetworkAvailable) {
ChatEmptyStateView() Spacer(modifier = Modifier.height(39.dp))
} else { Image(
NetworkErrorContentCompact( painter = painterResource(id = R.mipmap.invalid_name_3),
onReload = { contentDescription = "null data",
isLoading = true modifier = Modifier
// 重新加载所有类型的数据 .width(181.dp)
AgentChatListViewModel.refreshPager(context = context) .height(153.dp)
GroupChatListViewModel.refreshPager(context = context) )
FriendChatListViewModel.refreshPager(context = context) Spacer(modifier = Modifier.height(9.dp))
} Text(
) text = stringResource(R.string.no_one_knocked_yet),
} color = AppColors.text,
fontSize = 16.sp,
fontWeight = FontWeight.W600,
textAlign = TextAlign.Center,
modifier = Modifier.padding(horizontal = 24.dp),
maxLines = 2,
overflow = TextOverflow.Ellipsis
)
} else {
Spacer(modifier = Modifier.height(39.dp))
Image(
painter = painterResource(id = R.mipmap.invalid_name_10),
contentDescription = "network error",
modifier = Modifier
.size(181.dp)
)
Spacer(modifier = Modifier.height(24.dp))
Text(
text = stringResource(R.string.friend_chat_no_network_title),
color = AppColors.text,
fontSize = 16.sp,
fontWeight = FontWeight.W600,
textAlign = TextAlign.Center,
modifier = Modifier.padding(horizontal = 24.dp),
maxLines = 2,
overflow = TextOverflow.Ellipsis
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = stringResource(R.string.friend_chat_no_network_subtitle),
color = AppColors.secondaryText,
fontSize = 14.sp,
textAlign = TextAlign.Center,
modifier = Modifier.padding(horizontal = 24.dp),
maxLines = 3,
overflow = TextOverflow.Ellipsis
)
Spacer(modifier = Modifier.height(16.dp))
ReloadButton(
onClick = {
isLoading = true
// 重新加载所有类型的数据
AgentChatListViewModel.refreshPager(context = context)
GroupChatListViewModel.refreshPager(context = context)
FriendChatListViewModel.refreshPager(context = context)
}
)
} }
} }
} else { } else {

View File

@@ -33,7 +33,7 @@ import com.aiosman.ravenow.LocalNavController
import com.aiosman.ravenow.R import com.aiosman.ravenow.R
import com.aiosman.ravenow.ui.composables.CustomAsyncImage import com.aiosman.ravenow.ui.composables.CustomAsyncImage
import com.aiosman.ravenow.ui.composables.rememberDebouncer import com.aiosman.ravenow.ui.composables.rememberDebouncer
import com.aiosman.ravenow.ui.network.NetworkErrorContentCompact import com.aiosman.ravenow.ui.index.tabs.search.ReloadButton
import com.aiosman.ravenow.ui.modifiers.noRippleClickable import com.aiosman.ravenow.ui.modifiers.noRippleClickable
import com.aiosman.ravenow.utils.NetworkUtils import com.aiosman.ravenow.utils.NetworkUtils
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
@@ -41,7 +41,6 @@ import androidx.compose.ui.graphics.Brush
import androidx.compose.material.Button import androidx.compose.material.Button
import androidx.compose.material.ButtonDefaults import androidx.compose.material.ButtonDefaults
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import com.aiosman.ravenow.ui.index.tabs.profile.composable.ChatEmptyStateView
@OptIn(ExperimentalMaterialApi::class) @OptIn(ExperimentalMaterialApi::class)
@Composable @Composable
@@ -73,27 +72,70 @@ fun FriendChatListScreen() {
.pullRefresh(state) .pullRefresh(state)
) { ) {
if (FriendChatListViewModel.friendChatList.isEmpty() && !FriendChatListViewModel.isLoading) { if (FriendChatListViewModel.friendChatList.isEmpty() && !FriendChatListViewModel.isLoading) {
Box( Column(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.padding(horizontal = 16.dp), .padding(16.dp),
contentAlignment = Alignment.Center horizontalAlignment = Alignment.CenterHorizontally,
//verticalArrangement = Arrangement.Center
) { ) {
Column( val isNetworkAvailable = NetworkUtils.isNetworkAvailable(context)
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.offset(y = (-40).dp)
) {
val isNetworkAvailable = NetworkUtils.isNetworkAvailable(context)
if (isNetworkAvailable) { if (isNetworkAvailable) {
ChatEmptyStateView() Spacer(modifier = Modifier.height(39.dp))
} else { Image(
NetworkErrorContentCompact( painter = painterResource(id = R.mipmap.invalid_name_3),
onReload = { contentDescription = "null data",
FriendChatListViewModel.refreshPager(pullRefresh = true, context = context) modifier = Modifier
} .width(181.dp)
) .height(153.dp)
} )
Spacer(modifier = Modifier.height(9.dp))
Text(
text = stringResource(R.string.no_one_knocked_yet),
color = AppColors.text,
fontSize = 16.sp,
fontWeight = FontWeight.W600,
textAlign = TextAlign.Center,
modifier = Modifier.padding(horizontal = 24.dp),
maxLines = 2,
overflow = TextOverflow.Ellipsis
)
}else {
Spacer(modifier = Modifier.height(39.dp))
Image(
painter = painterResource(id = R.mipmap.invalid_name_10),
contentDescription = "network error",
modifier = Modifier
.size(181.dp)
)
Spacer(modifier = Modifier.height(24.dp))
Text(
text = stringResource(R.string.friend_chat_no_network_title),
color = AppColors.text,
fontSize = 16.sp,
fontWeight = FontWeight.W600,
textAlign = TextAlign.Center,
modifier = Modifier.padding(horizontal = 24.dp),
maxLines = 2,
overflow = TextOverflow.Ellipsis
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = stringResource(R.string.friend_chat_no_network_subtitle),
color = AppColors.secondaryText,
fontSize = 14.sp,
textAlign = TextAlign.Center,
modifier = Modifier.padding(horizontal = 24.dp),
maxLines = 3,
overflow = TextOverflow.Ellipsis
)
Spacer(modifier = Modifier.height(16.dp))
ReloadButton(
onClick = {
FriendChatListViewModel.refreshPager(pullRefresh = true, context = context)
}
)
} }
} }
} else { } else {
@@ -265,4 +307,43 @@ fun FriendChatItem(
} }
} }
} }
@Composable
fun ReloadButton(
onClick: () -> Unit
) {
val gradientBrush = Brush.linearGradient(
colors = listOf(
Color(0xFF7c45ed),
Color(0xFF7c68ef),
Color(0xFF7bd8f8)
)
)
Button(
onClick = onClick,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 120.dp)
.height(48.dp),
shape = RoundedCornerShape(30.dp),
colors = ButtonDefaults.buttonColors(
backgroundColor = Color.Transparent
),
contentPadding = PaddingValues(0.dp)
) {
Box(
modifier = Modifier
.fillMaxSize()
.background(gradientBrush),
contentAlignment = Alignment.Center
) {
Text(
text = stringResource(R.string.Reload),
fontSize = 16.sp,
fontWeight = FontWeight.Bold,
color = Color.White,
textAlign = TextAlign.Center
)
}
}
}

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
@@ -35,8 +35,7 @@ import com.aiosman.ravenow.ui.composables.CustomAsyncImage
import com.aiosman.ravenow.ui.composables.rememberDebouncer import com.aiosman.ravenow.ui.composables.rememberDebouncer
import com.aiosman.ravenow.ui.modifiers.noRippleClickable import com.aiosman.ravenow.ui.modifiers.noRippleClickable
import com.aiosman.ravenow.utils.NetworkUtils import com.aiosman.ravenow.utils.NetworkUtils
import com.aiosman.ravenow.ui.network.NetworkErrorContentCompact import com.aiosman.ravenow.ui.network.ReloadButton
import com.aiosman.ravenow.ui.index.tabs.profile.composable.ChatEmptyStateView
@OptIn(ExperimentalMaterialApi::class) @OptIn(ExperimentalMaterialApi::class)
@Composable @Composable
@@ -68,27 +67,69 @@ fun GroupChatListScreen() {
.pullRefresh(state) .pullRefresh(state)
) { ) {
if (GroupChatListViewModel.groupChatList.isEmpty() && !GroupChatListViewModel.isLoading) { if (GroupChatListViewModel.groupChatList.isEmpty() && !GroupChatListViewModel.isLoading) {
Box( Column(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.padding(horizontal = 16.dp), .padding(16.dp),
contentAlignment = Alignment.Center horizontalAlignment = Alignment.CenterHorizontally,
) { ) {
Column( val isNetworkAvailable = NetworkUtils.isNetworkAvailable(context)
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.offset(y = (-40).dp)
) {
val isNetworkAvailable = NetworkUtils.isNetworkAvailable(context)
if (isNetworkAvailable) { if (isNetworkAvailable) {
ChatEmptyStateView() Spacer(modifier = Modifier.height(39.dp))
} else { Image(
NetworkErrorContentCompact( painter = painterResource(id = R.mipmap.invalid_name_3),
onReload = { contentDescription = "null data",
GroupChatListViewModel.refreshPager(context = context) modifier = Modifier
} .width(181.dp)
) .height(153.dp)
} )
Spacer(modifier = Modifier.height(9.dp))
Text(
text = stringResource(R.string.no_one_knocked_yet),
color = AppColors.text,
fontSize = 16.sp,
fontWeight = FontWeight.W600,
textAlign = TextAlign.Center,
modifier = Modifier.padding(horizontal = 24.dp),
maxLines = 2,
overflow = TextOverflow.Ellipsis
)
}else {
Spacer(modifier = Modifier.height(39.dp))
Image(
painter = painterResource(id = R.mipmap.invalid_name_10),
contentDescription = "network error",
modifier = Modifier
.size(181.dp)
)
Spacer(modifier = Modifier.height(24.dp))
Text(
text = stringResource(R.string.friend_chat_no_network_title),
color = AppColors.text,
fontSize = 16.sp,
fontWeight = FontWeight.W600,
textAlign = TextAlign.Center,
modifier = Modifier.padding(horizontal = 24.dp),
maxLines = 2,
overflow = TextOverflow.Ellipsis
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = stringResource(R.string.friend_chat_no_network_subtitle),
color = AppColors.secondaryText,
fontSize = 14.sp,
textAlign = TextAlign.Center,
modifier = Modifier.padding(horizontal = 24.dp),
maxLines = 3,
overflow = TextOverflow.Ellipsis
)
Spacer(modifier = Modifier.height(16.dp))
ReloadButton(
onClick = {
GroupChatListViewModel.refreshPager(context = context)
}
)
} }
} }
} else { } else {
@@ -112,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()) {
@@ -172,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(
@@ -201,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(),
@@ -211,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(),
@@ -234,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

@@ -64,36 +64,12 @@ open class BaseMomentModel :ViewModel(){
momentLoader.updateMomentLike(event.postId, event.isLike) momentLoader.updateMomentLike(event.postId, event.isLike)
} }
suspend fun likeMoment(id: Int) { suspend fun likeMoment(id: Int) {
// 获取当前动态信息,用于计算新的点赞数
val currentMoment = momentLoader.list.find { it.id == id }
val newLikeCount = (currentMoment?.likeCount ?: 0) + 1
momentService.likeMoment(id) momentService.likeMoment(id)
momentLoader.updateMomentLike(id, true)
// 只发送事件,让事件订阅者统一处理更新,避免重复更新
EventBus.getDefault().post(
MomentLikeChangeEvent(
postId = id,
likeCount = newLikeCount,
isLike = true
)
)
} }
suspend fun dislikeMoment(id: Int) { suspend fun dislikeMoment(id: Int) {
// 获取当前动态信息,用于计算新的点赞数
val currentMoment = momentLoader.list.find { it.id == id }
val newLikeCount = ((currentMoment?.likeCount ?: 0) - 1).coerceAtLeast(0)
momentService.dislikeMoment(id) momentService.dislikeMoment(id)
momentLoader.updateMomentLike(id, false)
// 只发送事件,让事件订阅者统一处理更新,避免重复更新
EventBus.getDefault().post(
MomentLikeChangeEvent(
postId = id,
likeCount = newLikeCount,
isLike = false
)
)
} }
@@ -114,27 +90,14 @@ open class BaseMomentModel :ViewModel(){
suspend fun favoriteMoment(id: Int) { suspend fun favoriteMoment(id: Int) {
momentService.favoriteMoment(id) momentService.favoriteMoment(id)
momentLoader.updateFavoriteCount(id, true)
// 只发送事件,让事件订阅者统一处理更新,避免重复更新
EventBus.getDefault().post(
MomentFavouriteChangeEvent(
postId = id,
isFavourite = true
)
)
} }
suspend fun unfavoriteMoment(id: Int) { suspend fun unfavoriteMoment(id: Int) {
momentService.unfavoriteMoment(id) momentService.unfavoriteMoment(id)
momentLoader.updateFavoriteCount(id, false)
// 只发送事件,让事件订阅者统一处理更新,避免重复更新
EventBus.getDefault().post(
MomentFavouriteChangeEvent(
postId = id,
isFavourite = false
)
)
} }
@Subscribe @Subscribe

View File

@@ -17,7 +17,6 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Close import androidx.compose.material.icons.filled.Close
@@ -31,7 +30,6 @@ import androidx.compose.runtime.LaunchedEffect
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
@@ -146,8 +144,6 @@ fun NewsCommentModal(
var showCommentMenu by remember { mutableStateOf(false) } var showCommentMenu by remember { mutableStateOf(false) }
var contextComment by remember { mutableStateOf<CommentEntity?>(null) } var contextComment by remember { mutableStateOf<CommentEntity?>(null) }
var replyComment by remember { mutableStateOf<CommentEntity?>(null) } var replyComment by remember { mutableStateOf<CommentEntity?>(null) }
val listState = rememberLazyListState()
val scope = rememberCoroutineScope()
// 菜单弹窗 // 菜单弹窗
if (showCommentMenu) { if (showCommentMenu) {
@@ -230,14 +226,9 @@ fun NewsCommentModal(
.fillMaxWidth(), .fillMaxWidth(),
horizontalArrangement = Arrangement.End horizontalArrangement = Arrangement.End
) { ) {
OrderSelectionComponent( OrderSelectionComponent {
selectedOrder = commentViewModel.order
) {
commentViewModel.order = it commentViewModel.order = it
commentViewModel.reloadComment() commentViewModel.reloadComment()
scope.launch {
listState.scrollToItem(0)
}
} }
} }
} }
@@ -276,9 +267,7 @@ fun NewsCommentModal(
) )
} }
} else { } else {
LazyColumn( LazyColumn {
state = listState
) {
item { item {
CommentContent( CommentContent(
viewModel = commentViewModel, viewModel = commentViewModel,

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( Text(
verticalAlignment = Alignment.CenterVertically, text = "@${moment.nickname}",
modifier = Modifier.padding(bottom = 8.dp) modifier = Modifier
) { .padding(bottom = 8.dp)
Box( .noRippleClickable { navigateToProfile() },
modifier = Modifier fontSize = 16.sp,
.size(40.dp) color = Color.White,
.clip(CircleShape) style = TextStyle(fontWeight = FontWeight.Bold)
.background(Color.White.copy(alpha = 0.2f)) )
) {
CustomAsyncImage(
imageUrl = moment.avatar,
contentDescription = "用户头像",
modifier = Modifier.fillMaxSize(),
defaultRes = R.drawable.default_avatar
)
}
Text(
text = "@${moment.nickname}",
modifier = Modifier.padding(start = 8.dp),
fontSize = 16.sp,
color = Color.White,
style = TextStyle(fontWeight = FontWeight.Bold)
)
}
// 文字内容 // 文字内容
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,10 +239,12 @@ 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 { }
scope.launch { // 注意:不在这里增加评论数,应该在评论真正提交成功后再增加
RecommendViewModel.onAddComment(m.id) },
} onCommentAdded = { m ->
scope.launch {
RecommendViewModel.onAddComment(m.id)
} }
}, },
onFavoriteClick = { m -> onFavoriteClick = { m ->

View File

@@ -16,7 +16,10 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.PlayArrow
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.material3.rememberModalBottomSheetState
@@ -30,11 +33,8 @@ 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
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.compose.ui.platform.LocalLifecycleOwner
@@ -97,10 +97,6 @@ fun VideoRecommendationItem(
skipPartiallyExpanded = true skipPartiallyExpanded = true
) )
var pauseIconVisibleState by remember { mutableStateOf(false) } var pauseIconVisibleState by remember { mutableStateOf(false) }
var shouldResumeAfterLifecyclePause by remember { mutableStateOf(false) }
// 防抖:记录上次双击时间,防止快速重复双击
val lastDoubleTapTime = remember { mutableStateOf(0L) }
val doubleTapDebounceTime = 500L // 500ms 防抖时间
val exoPlayer = remember(videoUrl) { val exoPlayer = remember(videoUrl) {
ExoPlayer.Builder(context) ExoPlayer.Builder(context)
@@ -171,43 +167,29 @@ fun VideoRecommendationItem(
}, },
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.pointerInput(videoUrl, moment.liked) { .noRippleClickable {
detectTapGestures( pauseIconVisibleState = true
onDoubleTap = { offset -> exoPlayer.pause()
// 双击点赞/取消点赞 scope.launch {
val currentTime = System.currentTimeMillis() delay(100)
if (currentTime - lastDoubleTapTime.value > doubleTapDebounceTime) { if (exoPlayer.isPlaying) {
lastDoubleTapTime.value = currentTime
// 检查当前点赞状态,如果已点赞则取消点赞,如果未点赞则点赞
onLikeClick?.invoke(moment)
}
},
onTap = {
// 单击播放/暂停
pauseIconVisibleState = true
exoPlayer.pause() exoPlayer.pause()
scope.launch { } else {
delay(100) pauseIconVisibleState = false
if (exoPlayer.isPlaying) { exoPlayer.play()
exoPlayer.pause()
} else {
pauseIconVisibleState = false
exoPlayer.play()
}
}
} }
) }
} }
) )
if (pauseIconVisibleState) { if (pauseIconVisibleState) {
Image( Icon(
painter = painterResource(R.mipmap.dt_ts_sp_bf_btn), imageVector = Icons.Default.PlayArrow,
contentDescription = null, contentDescription = null,
modifier = Modifier modifier = Modifier
.align(Alignment.Center) .align(Alignment.Center)
.size(80.dp), .size(80.dp),
colorFilter = ColorFilter.tint(Color.White) tint = Color.White
) )
} }
} }
@@ -318,9 +300,7 @@ fun VideoRecommendationItem(
ModalBottomSheet( ModalBottomSheet(
onDismissRequest = { showCommentModal = false }, onDismissRequest = { showCommentModal = false },
containerColor = Color.White, containerColor = Color.White,
sheetState = sheetState, sheetState = sheetState
dragHandle = {},
shape = RoundedCornerShape(topStart = 28.dp, topEnd = 28.dp)
) { ) {
CommentModalContent(postId = moment.id) { CommentModalContent(postId = moment.id) {
// 评论添加后的回调 // 评论添加后的回调
@@ -340,18 +320,11 @@ fun VideoRecommendationItem(
val observer = LifecycleEventObserver { _, event -> val observer = LifecycleEventObserver { _, event ->
when (event) { when (event) {
Lifecycle.Event.ON_PAUSE -> { Lifecycle.Event.ON_PAUSE -> {
shouldResumeAfterLifecyclePause = exoPlayer.isPlaying && !pauseIconVisibleState
exoPlayer.pause() exoPlayer.pause()
} }
Lifecycle.Event.ON_RESUME -> { Lifecycle.Event.ON_RESUME -> {
if (isVisible && shouldResumeAfterLifecyclePause) { if (isVisible) {
pauseIconVisibleState = false
exoPlayer.play() exoPlayer.play()
} else {
// 未自动恢复播放时,如果当前可见且视频已暂停,则显示暂停图标
if (isVisible && !exoPlayer.isPlaying) {
pauseIconVisibleState = true
}
} }
} }
else -> {} else -> {}
@@ -426,4 +399,3 @@ private fun VideoBtn(
) )
} }
} }

View File

@@ -47,7 +47,6 @@ import com.aiosman.ravenow.ui.composables.rememberDebouncer
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import com.aiosman.ravenow.utils.NetworkUtils import com.aiosman.ravenow.utils.NetworkUtils
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import com.aiosman.ravenow.ui.network.NetworkErrorContentInline
/** /**
* 动态列表 * 动态列表
@@ -91,11 +90,38 @@ fun TimelineMomentsList() {
.padding(top = 188.dp), .padding(top = 188.dp),
contentAlignment = Alignment.TopCenter contentAlignment = Alignment.TopCenter
) { ) {
NetworkErrorContentInline( Column(
onReload = { horizontalAlignment = Alignment.CenterHorizontally,
model.refreshPager(pullRefresh = true) modifier = Modifier.fillMaxWidth()
} ) {
) val exploreDebouncer = rememberDebouncer()
Image(
painter = painterResource(id = R.mipmap.invalid_name_10),
contentDescription = "network error",
modifier = Modifier.size(140.dp)
)
Spacer(modifier = Modifier.size(24.dp))
Text(
text = stringResource(R.string.friend_chat_no_network_title),
color = AppColors.text,
fontSize = 16.sp,
fontWeight = FontWeight.W600
)
Spacer(modifier = Modifier.size(8.dp))
Text(
text = stringResource(R.string.friend_chat_no_network_subtitle),
color = AppColors.text,
fontSize = 16.sp,
fontWeight = FontWeight.W400
)
Spacer(modifier = Modifier.size(16.dp))
ExploreButton(
onClick = {
exploreDebouncer {
/* TODO: 添加点击事件处理 */
} }
)
}
} }
} else if (moments.isEmpty()) { } else if (moments.isEmpty()) {
Box( Box(

View File

@@ -33,8 +33,6 @@ import com.aiosman.ravenow.event.MomentFavouriteChangeEvent
import com.aiosman.ravenow.event.MomentLikeChangeEvent import com.aiosman.ravenow.event.MomentLikeChangeEvent
import com.aiosman.ravenow.event.MomentRemoveEvent import com.aiosman.ravenow.event.MomentRemoveEvent
import com.aiosman.ravenow.data.PointService import com.aiosman.ravenow.data.PointService
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.greenrobot.eventbus.EventBus import org.greenrobot.eventbus.EventBus
import org.greenrobot.eventbus.Subscribe import org.greenrobot.eventbus.Subscribe
@@ -310,16 +308,14 @@ object MyProfileViewModel : ViewModel() {
* 加载房间列表 * 加载房间列表
* @param filterType 筛选类型0=全部1=公开2=私有 * @param filterType 筛选类型0=全部1=公开2=私有
* @param pullRefresh 是否下拉刷新 * @param pullRefresh 是否下拉刷新
* @param ownerSessionId 创建者用户IDChatAIID用于过滤特定创建者的房间。如果为null则显示当前用户创建或加入的房间
*/ */
fun loadRooms(filterType: Int = 0, pullRefresh: Boolean = false, ownerSessionId: String? = null) { fun loadRooms(filterType: Int = 0, pullRefresh: Boolean = false) {
// 游客模式下不加载房间列表 // 游客模式下不加载房间列表
if (AppStore.isGuest) { if (AppStore.isGuest) {
Log.d("MyProfileViewModel", "loadRooms: 游客模式下跳过加载房间列表") Log.d("MyProfileViewModel", "loadRooms: 游客模式下跳过加载房间列表")
return return
} }
val normalizedOwnerId = normalizeOwnerSessionId(ownerSessionId)
if (roomsLoading && !pullRefresh) return if (roomsLoading && !pullRefresh) return
viewModelScope.launch { viewModelScope.launch {
@@ -335,69 +331,60 @@ object MyProfileViewModel : ViewModel() {
roomsCurrentPage roomsCurrentPage
} }
// 根据filterType确定roomType val response = when (filterType) {
val roomType = when (filterType) { 0 -> {
1 -> "public" // 全部:显示自己创建或加入的所有房间
2 -> "private" apiClient.getRooms(
else -> null page = currentPage,
} pageSize = roomsPageSize,
val effectiveRoomType = if (normalizedOwnerId != null) "public" else roomType showCreated = true,
showJoined = true
// 构建API调用参数 )
if (normalizedOwnerId != null) {
// 查看其他用户的房间:仅显示该用户创建的公开房间
val createdResponse = apiClient.getRooms(
page = currentPage,
pageSize = roomsPageSize,
roomType = effectiveRoomType,
ownerSessionId = normalizedOwnerId,
includeUsers = false
)
val createdRooms = if (createdResponse.isSuccessful) {
createdResponse.body()?.list?.map { it.toRoomtEntity() } ?: emptyList()
} else {
emptyList()
} }
1 -> {
// 公开:显示公开房间中自己创建或加入的
apiClient.getRooms(
page = currentPage,
pageSize = roomsPageSize,
roomType = "public",
showCreated = true,
showJoined = true
)
}
2 -> {
// 私有:显示自己创建或加入的私有房间
apiClient.getRooms(
page = currentPage,
pageSize = roomsPageSize,
roomType = "private"
)
}
else -> {
apiClient.getRooms(
page = currentPage,
pageSize = roomsPageSize,
showCreated = true,
showJoined = true
)
}
}
if (response.isSuccessful) {
val roomList = response.body()?.list ?: emptyList()
val total = response.body()?.total ?: 0L
if (pullRefresh || currentPage == 1) { if (pullRefresh || currentPage == 1) {
rooms = createdRooms rooms = roomList.map { it.toRoomtEntity() }
} else { } else {
rooms = rooms + createdRooms rooms = rooms + roomList.map { it.toRoomtEntity() }
} }
val total = createdResponse.body()?.total ?: 0L
roomsHasMore = rooms.size < total roomsHasMore = rooms.size < total
if (roomsHasMore && !pullRefresh) { if (roomsHasMore && !pullRefresh) {
roomsCurrentPage++ roomsCurrentPage++
} }
} else { } else {
// 查看自己的房间:显示创建或加入的房间 Log.e("MyProfileViewModel", "loadRooms failed: ${response.code()}")
val response = apiClient.getRooms(
page = currentPage,
pageSize = roomsPageSize,
roomType = effectiveRoomType,
showCreated = true,
showJoined = if (filterType == 2) null else true // 私有房间不显示加入的
)
if (response.isSuccessful) {
val roomList = response.body()?.list ?: emptyList()
val total = response.body()?.total ?: 0L
if (pullRefresh || currentPage == 1) {
rooms = roomList.map { it.toRoomtEntity() }
} else {
rooms = rooms + roomList.map { it.toRoomtEntity() }
}
roomsHasMore = rooms.size < total
if (roomsHasMore && !pullRefresh) {
roomsCurrentPage++
}
} else {
Log.e("MyProfileViewModel", "loadRooms failed: ${response.code()}")
}
} }
} catch (e: Exception) { } catch (e: Exception) {
Log.e("MyProfileViewModel", "loadRooms error: ", e) Log.e("MyProfileViewModel", "loadRooms error: ", e)
@@ -411,29 +398,20 @@ object MyProfileViewModel : ViewModel() {
/** /**
* 加载更多房间 * 加载更多房间
* @param filterType 筛选类型0=全部1=公开2=私有 * @param filterType 筛选类型0=全部1=公开2=私有
* @param ownerSessionId 创建者用户IDChatAIID用于过滤特定创建者的房间
*/ */
fun loadMoreRooms(filterType: Int = 0, ownerSessionId: String? = null) { fun loadMoreRooms(filterType: Int = 0) {
val normalizedOwnerId = normalizeOwnerSessionId(ownerSessionId)
if (roomsLoading || !roomsHasMore) return if (roomsLoading || !roomsHasMore) return
loadRooms(filterType = filterType, pullRefresh = false, ownerSessionId = normalizedOwnerId) loadRooms(filterType = filterType, pullRefresh = false)
} }
/** /**
* 刷新房间列表 * 刷新房间列表
* @param filterType 筛选类型0=全部1=公开2=私有 * @param filterType 筛选类型0=全部1=公开2=私有
* @param ownerSessionId 创建者用户IDChatAIID用于过滤特定创建者的房间
*/ */
fun refreshRooms(filterType: Int = 0, ownerSessionId: String? = null) { fun refreshRooms(filterType: Int = 0) {
rooms = emptyList() rooms = emptyList()
roomsCurrentPage = 1 roomsCurrentPage = 1
roomsHasMore = true roomsHasMore = true
val normalizedOwnerId = normalizeOwnerSessionId(ownerSessionId) loadRooms(filterType = filterType, pullRefresh = true)
loadRooms(filterType = filterType, pullRefresh = true, ownerSessionId = normalizedOwnerId)
}
private fun normalizeOwnerSessionId(ownerSessionId: String?): String? {
val trimmed = ownerSessionId?.trim()
return if (trimmed.isNullOrEmpty()) null else trimmed
} }
} }

View File

@@ -52,7 +52,6 @@ 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
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
@@ -60,8 +59,6 @@ import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
import androidx.compose.ui.input.nestedscroll.NestedScrollSource import androidx.compose.ui.input.nestedscroll.NestedScrollSource
import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.layout.positionInRoot
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
@@ -88,8 +85,6 @@ import com.aiosman.ravenow.ui.index.IndexViewModel
import com.aiosman.ravenow.ui.index.tabs.profile.composable.GalleryGrid import com.aiosman.ravenow.ui.index.tabs.profile.composable.GalleryGrid
import com.aiosman.ravenow.ui.index.tabs.profile.composable.GroupChatEmptyContent import com.aiosman.ravenow.ui.index.tabs.profile.composable.GroupChatEmptyContent
import com.aiosman.ravenow.ui.index.tabs.profile.composable.OtherProfileAction import com.aiosman.ravenow.ui.index.tabs.profile.composable.OtherProfileAction
import com.aiosman.ravenow.ui.index.tabs.profile.composable.SegmentedControl
import com.aiosman.ravenow.ui.index.tabs.profile.composable.AgentSegmentedControl
import com.aiosman.ravenow.ui.index.tabs.profile.composable.UserAgentsList import com.aiosman.ravenow.ui.index.tabs.profile.composable.UserAgentsList
import com.aiosman.ravenow.ui.index.tabs.profile.composable.UserAgentsRow import com.aiosman.ravenow.ui.index.tabs.profile.composable.UserAgentsRow
import com.aiosman.ravenow.ui.index.tabs.profile.composable.UserContentPageIndicator import com.aiosman.ravenow.ui.index.tabs.profile.composable.UserContentPageIndicator
@@ -173,41 +168,6 @@ fun ProfileV3(
initialFirstVisibleItemScrollOffset = model.profileGridFirstVisibleItemOffset initialFirstVisibleItemScrollOffset = model.profileGridFirstVisibleItemOffset
) )
val scrollState = rememberScrollState(model.profileScrollOffset) val scrollState = rememberScrollState(model.profileScrollOffset)
var tabIndicatorContentOffset by remember { mutableStateOf<Float?>(null) }
var tabIndicatorHeightPx by remember { mutableStateOf(0) }
val topNavigationBarHeightPx = with(density) { (statusBarPaddingValues.calculateTopPadding() + 56.dp).toPx() }
val stickyTopPadding = statusBarPaddingValues.calculateTopPadding() + 56.dp
var agentSegmentOffset by remember { mutableStateOf<Float?>(null) }
var agentSegmentHeightPx by remember { mutableStateOf(0) }
var groupSegmentOffset by remember { mutableStateOf<Float?>(null) }
var groupSegmentHeightPx by remember { mutableStateOf(0) }
var agentSegmentSelected by remember { mutableStateOf(0) }
var groupSegmentSelected by remember { mutableStateOf(0) }
val tabIndicatorHeightDp = with(density) { tabIndicatorHeightPx.toDp() }
val tabBarBottomPx = topNavigationBarHeightPx + tabIndicatorHeightPx
val tabBarBottomPadding = stickyTopPadding + tabIndicatorHeightDp
val tabStickyThreshold = remember(tabIndicatorContentOffset, topNavigationBarHeightPx) {
tabIndicatorContentOffset?.minus(topNavigationBarHeightPx)
}
val agentSegmentThreshold = remember(agentSegmentOffset, tabBarBottomPx) {
agentSegmentOffset?.minus(tabBarBottomPx)
}
val groupSegmentThreshold = remember(groupSegmentOffset, tabBarBottomPx) {
groupSegmentOffset?.minus(tabBarBottomPx)
}
val agentTabIndex = if (isAiAccount) -1 else 1
val groupTabIndex = if (isAiAccount) 1 else 2
val shouldStickTabBar = tabStickyThreshold?.let { scrollState.value >= it } ?: false
val shouldStickAgentSegments = isSelf && !isAiAccount && agentSegmentThreshold?.let { scrollState.value >= it } == true && pagerState.currentPage == agentTabIndex
val shouldStickGroupSegments = isSelf && groupSegmentThreshold?.let { scrollState.value >= it } == true && pagerState.currentPage == groupTabIndex
val externalOwnerSessionId = remember(isSelf, profile?.chatAIId, profile?.trtcUserId) {
if (isSelf) {
null
} else {
profile?.chatAIId?.takeIf { it.isNotBlank() }
?: profile?.trtcUserId?.takeIf { it.isNotBlank() }
}
}
val nestedScrollConnection = remember(scrollState, pagerState, gridState, listState, groupChatListState, isAiAccount) { val nestedScrollConnection = remember(scrollState, pagerState, gridState, listState, groupChatListState, isAiAccount) {
object : NestedScrollConnection { object : NestedScrollConnection {
override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset { override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
@@ -532,19 +492,10 @@ fun ProfileV3(
.background(AppColors.profileBackground) .background(AppColors.profileBackground)
.padding(top = 8.dp) .padding(top = 8.dp)
) { ) {
Box( UserContentPageIndicator(
modifier = Modifier pagerState = pagerState,
.onGloballyPositioned { coordinates -> showAgentTab = !isAiAccount
tabIndicatorHeightPx = coordinates.size.height )
tabIndicatorContentOffset = coordinates.positionInRoot().y + scrollState.value
}
.alpha(if (shouldStickTabBar) 0f else 1f)
) {
UserContentPageIndicator(
pagerState = pagerState,
showAgentTab = !isAiAccount
)
}
HorizontalPager( HorizontalPager(
state = pagerState, state = pagerState,
modifier = Modifier.height(650.dp) // 固定滚动高度 modifier = Modifier.height(650.dp) // 固定滚动高度
@@ -582,53 +533,22 @@ fun ProfileV3(
showNoMoreText = isSelf, showNoMoreText = isSelf,
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
state = listState, state = listState,
nestedScrollConnection = nestedScrollConnection, nestedScrollConnection = nestedScrollConnection
showSegments = isSelf, // 只有查看自己的主页时才显示分段控制器
segmentSelectedIndex = agentSegmentSelected,
onSegmentSelected = { agentSegmentSelected = it },
onSegmentMeasured = { offset, height ->
agentSegmentOffset = offset
agentSegmentHeightPx = height
},
isSegmentSticky = shouldStickAgentSegments,
parentScrollProvider = { scrollState.value }
) )
} else { } else {
// 查看其他用户的主页时传递该用户的会话ID以显示其创建的群聊查看自己的主页时传递null
GroupChatPlaceholder( GroupChatPlaceholder(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
listState = groupChatListState, listState = groupChatListState,
nestedScrollConnection = nestedScrollConnection, nestedScrollConnection = nestedScrollConnection
ownerSessionId = externalOwnerSessionId,
showSegments = isSelf, // 只有查看自己的主页时才显示分段控制器
selectedSegmentIndex = groupSegmentSelected,
onSegmentSelected = { groupSegmentSelected = it },
onSegmentMeasured = { offset, height ->
groupSegmentOffset = offset
groupSegmentHeightPx = height
},
isSegmentSticky = shouldStickGroupSegments,
parentScrollProvider = { scrollState.value }
) )
} }
} }
2 -> { 2 -> {
if (!isAiAccount) { if (!isAiAccount) {
// 查看其他用户的主页时传递该用户的会话ID以显示其创建的群聊查看自己的主页时传递null
GroupChatPlaceholder( GroupChatPlaceholder(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
listState = groupChatListState, listState = groupChatListState,
nestedScrollConnection = nestedScrollConnection, nestedScrollConnection = nestedScrollConnection
ownerSessionId = externalOwnerSessionId,
showSegments = isSelf, // 只有查看自己的主页时才显示分段控制器
selectedSegmentIndex = groupSegmentSelected,
onSegmentSelected = { groupSegmentSelected = it },
onSegmentMeasured = { offset, height ->
groupSegmentOffset = offset
groupSegmentHeightPx = height
},
isSegmentSticky = shouldStickGroupSegments,
parentScrollProvider = { scrollState.value }
) )
} }
} }
@@ -640,55 +560,6 @@ fun ProfileV3(
Spacer(modifier = Modifier.height(16.dp)) Spacer(modifier = Modifier.height(16.dp))
} }
if (shouldStickTabBar && tabIndicatorHeightPx > 0) {
Box(
modifier = Modifier
.fillMaxWidth()
.align(Alignment.TopCenter)
.padding(top = stickyTopPadding)
.background(AppColors.profileBackground)
) {
UserContentPageIndicator(
pagerState = pagerState,
showAgentTab = !isAiAccount
)
}
}
if (shouldStickAgentSegments && agentSegmentHeightPx > 0) {
Box(
modifier = Modifier
.fillMaxWidth()
.align(Alignment.TopCenter)
.padding(top = tabBarBottomPadding)
.background(AppColors.profileBackground)
.padding(horizontal = 16.dp)
) {
AgentSegmentedControl(
selectedIndex = agentSegmentSelected,
onSegmentSelected = { agentSegmentSelected = it },
modifier = Modifier.fillMaxWidth()
)
}
}
if (shouldStickGroupSegments && groupSegmentHeightPx > 0) {
Box(
modifier = Modifier
.fillMaxWidth()
.align(Alignment.TopCenter)
.padding(top = tabBarBottomPadding)
.background(AppColors.profileBackground)
.padding(horizontal = 16.dp)
) {
SegmentedControl(
selectedIndex = groupSegmentSelected,
onSegmentSelected = { groupSegmentSelected = it },
modifier = Modifier.fillMaxWidth()
)
}
}
// 顶部导航栏 // 顶部导航栏
TopNavigationBar( TopNavigationBar(
isMain = isMain, isMain = isMain,
@@ -785,26 +656,12 @@ fun ProfileV3(
private fun GroupChatPlaceholder( private fun GroupChatPlaceholder(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
listState: androidx.compose.foundation.lazy.LazyListState, listState: androidx.compose.foundation.lazy.LazyListState,
nestedScrollConnection: NestedScrollConnection? = null, nestedScrollConnection: NestedScrollConnection? = null
ownerSessionId: String? = null, // 创建者用户IDChatAIID用于过滤特定创建者的房间。如果为null则显示当前用户创建或加入的房间
showSegments: Boolean = true,
selectedSegmentIndex: Int = 0,
onSegmentSelected: (Int) -> Unit = {},
onSegmentMeasured: ((Float, Int) -> Unit)? = null,
isSegmentSticky: Boolean = false,
parentScrollProvider: () -> Int = { 0 }
) { ) {
GroupChatEmptyContent( GroupChatEmptyContent(
modifier = modifier, modifier = modifier,
listState = listState, listState = listState,
nestedScrollConnection = nestedScrollConnection, nestedScrollConnection = nestedScrollConnection
ownerSessionId = ownerSessionId,
showSegments = showSegments,
selectedSegmentIndex = selectedSegmentIndex,
onSegmentSelected = onSegmentSelected,
onSegmentMeasured = onSegmentMeasured,
isSegmentSticky = isSegmentSticky,
parentScrollProvider = parentScrollProvider
) )
} }

View File

@@ -1,102 +0,0 @@
package com.aiosman.ravenow.ui.index.tabs.profile.composable
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
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 com.aiosman.ravenow.LocalAppTheme
import com.aiosman.ravenow.R
/**
* 空状态缺省图组件
* 用于显示我的-动态、智能体、群聊无内容时的缺省图片和提示文本
*/
@Composable
fun EmptyStateView(
modifier: Modifier = Modifier,
contentDescription: String = "暂无内容",
text: String = stringResource(R.string.cosmos_awaits),
fontWeight: FontWeight = FontWeight.W600
) {
val AppColors = LocalAppTheme.current
Column(
modifier = modifier
.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally
) {
Image(
painter = painterResource(id = R.mipmap.l_empty_img),
contentDescription = contentDescription,
modifier = Modifier
.size(width = 181.dp, height = 153.dp),
contentScale = ContentScale.Fit
)
Spacer(modifier = Modifier.height(9.dp))
Text(
text = text,
fontSize = 16.sp,
color = AppColors.text,
fontWeight = fontWeight,
textAlign = TextAlign.Center,
modifier = Modifier.padding(horizontal = 24.dp),
maxLines = 2,
overflow = TextOverflow.Ellipsis
)
}
}
/**
* 消息页统一空状态
*/
@Composable
fun ChatEmptyStateView(
modifier: Modifier = Modifier,
contentDescription: String = "暂无会话",
text: String = stringResource(R.string.no_one_knocked_yet)
) {
val AppColors = LocalAppTheme.current
Column(
modifier = modifier
.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally
) {
Image(
painter = painterResource(id = R.mipmap.invalid_name_3),
contentDescription = contentDescription,
modifier = Modifier.size(120.dp)
)
Spacer(modifier = Modifier.height(12.dp))
Text(
text = text,
color = AppColors.text,
fontSize = 16.sp,
fontWeight = FontWeight.W600,
textAlign = TextAlign.Center,
modifier = Modifier.padding(horizontal = 24.dp),
maxLines = 2,
overflow = TextOverflow.Ellipsis
)
}
}

View File

@@ -1,50 +1,47 @@
package com.aiosman.ravenow.ui.index.tabs.profile.composable package com.aiosman.ravenow.ui.index.tabs.profile.composable
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import com.aiosman.ravenow.LocalNavController
import com.aiosman.ravenow.entity.MomentEntity
import com.aiosman.ravenow.ui.composables.CustomAsyncImage
import com.aiosman.ravenow.ui.modifiers.noRippleClickable
import com.aiosman.ravenow.ui.navigateToPost
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.ui.Alignment
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.sp
import com.aiosman.ravenow.LocalAppTheme
import com.aiosman.ravenow.R
import androidx.compose.material3.Text
import androidx.compose.foundation.lazy.grid.GridCells import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyGridState import androidx.compose.foundation.lazy.grid.LazyGridState
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.itemsIndexed import androidx.compose.foundation.lazy.grid.itemsIndexed
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.PlayArrow
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
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.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.aiosman.ravenow.LocalAppTheme
import com.aiosman.ravenow.LocalNavController
import com.aiosman.ravenow.R
import com.aiosman.ravenow.entity.MomentEntity
import com.aiosman.ravenow.ui.composables.CustomAsyncImage
import com.aiosman.ravenow.ui.modifiers.noRippleClickable
import com.aiosman.ravenow.ui.navigateToPost
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import com.aiosman.ravenow.AppState import com.aiosman.ravenow.AppState
@@ -158,8 +155,7 @@ fun GalleryGrid(
modifier = baseModifier modifier = baseModifier
.fillMaxSize() .fillMaxSize()
.verticalScroll(rememberScrollState()) .verticalScroll(rememberScrollState())
.padding(vertical = 60.dp) .padding(vertical = 60.dp),
.padding(horizontal = 16.dp),
horizontalAlignment = Alignment.CenterHorizontally horizontalAlignment = Alignment.CenterHorizontally
) { ) {
Image( Image(
@@ -202,8 +198,24 @@ fun GalleryGrid(
.padding(vertical = 60.dp), .padding(vertical = 60.dp),
horizontalAlignment = Alignment.CenterHorizontally horizontalAlignment = Alignment.CenterHorizontally
) { ) {
EmptyStateView( Image(
contentDescription = "暂无图片" painter = painterResource(id = R.mipmap.l_empty_img),
contentDescription = "暂无图片",
modifier = Modifier
.size(width = 181.dp, height = 153.dp),
)
Spacer(modifier = Modifier.height(9.dp))
Text(
text = stringResource(R.string.cosmos_awaits),
fontSize = 16.sp,
color = AppColors.text,
fontWeight = FontWeight.W600,
textAlign = TextAlign.Center,
modifier = Modifier.padding(horizontal = 24.dp),
maxLines = 2,
overflow = TextOverflow.Ellipsis
) )
} }
} else { } else {
@@ -215,20 +227,8 @@ fun GalleryGrid(
.padding(bottom = 8.dp), .padding(bottom = 8.dp),
) { ) {
itemsIndexed(moments) { idx, moment -> itemsIndexed(moments) { idx, moment ->
moment?.let { momentItem -> if (moment != null && moment.images.isNotEmpty()) {
val itemDebouncer = rememberDebouncer() val itemDebouncer = rememberDebouncer()
val isVideoMoment = momentItem.images.isEmpty() && !momentItem.videos.isNullOrEmpty()
val previewUrl = when {
momentItem.images.isNotEmpty() -> momentItem.images[0].thumbnail
isVideoMoment -> {
val firstVideo = momentItem.videos!!.first()
firstVideo.thumbnailDirectUrl
?: firstVideo.thumbnailUrl
?: firstVideo.directUrl
?: firstVideo.url
}
else -> null
}
Box( Box(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
@@ -237,32 +237,20 @@ fun GalleryGrid(
.noRippleClickable { .noRippleClickable {
itemDebouncer { itemDebouncer {
navController.navigateToPost( navController.navigateToPost(
id = momentItem.id, id = moment.id,
highlightCommentId = 0, highlightCommentId = 0,
initImagePagerIndex = 0 initImagePagerIndex = 0
) )
} }
} }
) { ) {
if (previewUrl != null) { CustomAsyncImage(
CustomAsyncImage( imageUrl = moment.images[0].thumbnail,
imageUrl = previewUrl, contentDescription = "",
contentDescription = "", modifier = Modifier.fillMaxSize(),
modifier = Modifier.fillMaxSize(), context = LocalContext.current
context = LocalContext.current )
) if (moment.images.size > 1) {
} else {
Box(
modifier = Modifier
.fillMaxSize()
.background(
color = AppColors.basicMain.copy(alpha = 0.2f),
shape = RoundedCornerShape(10.dp)
)
)
}
if (momentItem.images.size > 1 || (momentItem.videos?.size ?: 0) > 1) {
Box( Box(
modifier = Modifier modifier = Modifier
.padding(top = 8.dp, end = 8.dp) .padding(top = 8.dp, end = 8.dp)
@@ -275,31 +263,6 @@ fun GalleryGrid(
) )
} }
} }
if (isVideoMoment) {
Box(
modifier = Modifier
.padding(top = 8.dp, end = 8.dp)
.align(Alignment.TopEnd)
) {
Box(
modifier = Modifier
.size(24.dp)
.background(
color = Color.Black.copy(alpha = 0.4f),
shape = RoundedCornerShape(12.dp)
),
contentAlignment = Alignment.Center
) {
Icon(
imageVector = Icons.Default.PlayArrow,
contentDescription = "",
tint = Color.White,
modifier = Modifier.size(16.dp)
)
}
}
}
} }
} }
} }

View File

@@ -16,16 +16,15 @@ import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
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.itemsIndexed
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.pullrefresh.PullRefreshIndicator 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.ui.graphics.ColorFilter
import androidx.compose.ui.draw.alpha
import com.airbnb.lottie.compose.LottieAnimation import com.airbnb.lottie.compose.LottieAnimation
import com.airbnb.lottie.compose.LottieCompositionSpec import com.airbnb.lottie.compose.LottieCompositionSpec
import com.airbnb.lottie.compose.LottieConstants import com.airbnb.lottie.compose.LottieConstants
@@ -41,8 +40,6 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.layout.positionInRoot
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
@@ -66,47 +63,37 @@ import com.aiosman.ravenow.ui.navigateToGroupChat
import com.aiosman.ravenow.AppStore import com.aiosman.ravenow.AppStore
import com.aiosman.ravenow.data.api.ApiClient import com.aiosman.ravenow.data.api.ApiClient
import android.util.Base64 import android.util.Base64
import com.aiosman.ravenow.ui.network.ReloadButton
import com.aiosman.ravenow.utils.NetworkUtils.isNetworkAvailable
@OptIn(ExperimentalMaterialApi::class) @OptIn(ExperimentalMaterialApi::class)
@Composable @Composable
fun GroupChatEmptyContent( fun GroupChatEmptyContent(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
listState: LazyListState, listState: LazyListState,
nestedScrollConnection: NestedScrollConnection? = null, nestedScrollConnection: NestedScrollConnection? = null
ownerSessionId: String? = null, // 创建者用户IDChatAIID用于过滤特定创建者的房间。如果为null则显示当前用户创建或加入的房间
showSegments: Boolean = true, // 是否显示分段控制器(全部、公开、私有)
selectedSegmentIndex: Int = 0,
onSegmentSelected: (Int) -> Unit = {},
onSegmentMeasured: ((Float, Int) -> Unit)? = null,
isSegmentSticky: Boolean = false,
parentScrollProvider: () -> Int = { 0 }
) { ) {
var selectedSegment by remember { mutableStateOf(0) } // 0: 全部, 1: 公开, 2: 私有
val AppColors = LocalAppTheme.current val AppColors = LocalAppTheme.current
val context = LocalContext.current val context = LocalContext.current
val navController = LocalNavController.current val navController = LocalNavController.current
val viewModel = MyProfileViewModel val viewModel = MyProfileViewModel
val normalizedOwnerSessionId = ownerSessionId?.takeIf { it.isNotBlank() }
val canLoadRooms = showSegments || normalizedOwnerSessionId != null
val networkAvailable = isNetworkAvailable(context)
// 如果查看其他用户的房间固定使用全部类型filterType = 0
val filterType = if (showSegments) selectedSegmentIndex else 0
val state = rememberPullRefreshState( val state = rememberPullRefreshState(
refreshing = if (canLoadRooms) viewModel.roomsRefreshing else false, refreshing = viewModel.roomsRefreshing,
onRefresh = { onRefresh = {
if (canLoadRooms) { viewModel.refreshRooms(filterType = selectedSegment)
viewModel.refreshRooms(filterType = filterType, ownerSessionId = normalizedOwnerSessionId)
}
} }
) )
// 当分段或用户ID改变时,重新加载数据 // 当分段改变时,重新加载数据
LaunchedEffect(selectedSegmentIndex, normalizedOwnerSessionId, showSegments) { LaunchedEffect(selectedSegment) {
if (canLoadRooms) { // 切换分段时重新加载
viewModel.refreshRooms(filterType = filterType, ownerSessionId = normalizedOwnerSessionId) viewModel.refreshRooms(filterType = selectedSegment)
}
// 初始加载
LaunchedEffect(Unit) {
if (viewModel.rooms.isEmpty() && !viewModel.roomsLoading) {
viewModel.loadRooms(filterType = selectedSegment)
} }
} }
@@ -123,90 +110,72 @@ fun GroupChatEmptyContent(
) { ) {
Spacer(modifier = Modifier.height(16.dp)) Spacer(modifier = Modifier.height(16.dp))
// 只在查看自己的房间时显示分段控制器 // 分段控制器
if (showSegments) { SegmentedControl(
SegmentedControl( selectedIndex = selectedSegment,
selectedIndex = selectedSegmentIndex, onSegmentSelected = {
onSegmentSelected = onSegmentSelected, selectedSegment = it
modifier = Modifier // LaunchedEffect 会监听 selectedSegment 的变化并自动刷新
.fillMaxWidth() },
.onGloballyPositioned { coordinates -> modifier = Modifier.fillMaxWidth()
onSegmentMeasured?.invoke( )
coordinates.positionInRoot().y + parentScrollProvider(),
coordinates.size.height
)
}
.alpha(if (isSegmentSticky) 0f else 1f)
)
Spacer(modifier = Modifier.height(8.dp)) Spacer(modifier = Modifier.height(8.dp))
}
Box( Box(
modifier = nestedScrollModifier modifier = nestedScrollModifier
.fillMaxSize() .fillMaxSize()
.pullRefresh(state) .pullRefresh(state)
) { ) {
if (!canLoadRooms) { if (viewModel.rooms.isEmpty() && !viewModel.roomsLoading) {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
CircularProgressIndicator()
}
} else if (viewModel.rooms.isEmpty() && !viewModel.roomsLoading) {
// 空状态内容(居中) // 空状态内容(居中)
Column( Column(
modifier = nestedScrollModifier.fillMaxWidth(), modifier = nestedScrollModifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally horizontalAlignment = Alignment.CenterHorizontally
) { ) {
// 空状态插图 // 空状态插图
EmptyStateIllustration( EmptyStateIllustration()
isNetworkAvailable = networkAvailable,
onReload = {
MyProfileViewModel.ResetModel()
MyProfileViewModel.loadProfile(pullRefresh = true)
}
)
Spacer(modifier = Modifier.height(9.dp)) Spacer(modifier = Modifier.height(9.dp))
// 空状态文本
Text(
text = stringResource(R.string.cosmos_awaits),
fontSize = 16.sp,
fontWeight = FontWeight.SemiBold,
color = AppColors.text,
textAlign = TextAlign.Center,
modifier = Modifier.padding(horizontal = 24.dp),
maxLines = 2,
overflow = TextOverflow.Ellipsis
)
} }
} else { } else {
LazyColumn( LazyColumn(
state = listState, state = listState,
modifier = nestedScrollModifier.fillMaxSize() modifier = nestedScrollModifier.fillMaxSize()
) { ) {
// 网格布局每行显示2个房间卡片 itemsIndexed(
items( items = viewModel.rooms,
items = viewModel.rooms.chunked(2), key = { _, item -> item.id }
key = { rowRooms -> rowRooms.firstOrNull()?.id?.toString() ?: "" } ) { index, room ->
) { rowRooms -> RoomItem(
Row( room = room,
modifier = Modifier onRoomClick = { roomEntity ->
.fillMaxWidth() // 导航到群聊聊天界面
.padding(bottom = 12.dp), navController.navigateToGroupChat(
horizontalArrangement = Arrangement.spacedBy(12.dp) id = roomEntity.trtcRoomId,
) { name = roomEntity.name,
rowRooms.forEach { room -> avatar = roomEntity.avatar
RoomCard(
room = room,
onRoomClick = { roomEntity ->
// 导航到群聊聊天界面
navController.navigateToGroupChat(
id = roomEntity.trtcRoomId,
name = roomEntity.name,
avatar = roomEntity.avatar
)
},
modifier = Modifier.weight(1f)
) )
} }
// 如果这一行只有一个房间,添加一个空的占位符 )
if (rowRooms.size == 1) {
Spacer(modifier = Modifier.weight(1f)) if (index < viewModel.rooms.size - 1) {
} HorizontalDivider(
modifier = Modifier.padding(horizontal = 24.dp),
color = AppColors.divider
)
} }
} }
@@ -232,141 +201,22 @@ fun GroupChatEmptyContent(
if (viewModel.roomsHasMore && !viewModel.roomsLoading) { if (viewModel.roomsHasMore && !viewModel.roomsLoading) {
item { item {
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
viewModel.loadMoreRooms( viewModel.loadMoreRooms(filterType = selectedSegment)
filterType = filterType,
ownerSessionId = normalizedOwnerSessionId
)
} }
} }
} }
} }
} }
if (canLoadRooms) { PullRefreshIndicator(
PullRefreshIndicator( refreshing = viewModel.roomsRefreshing,
refreshing = viewModel.roomsRefreshing, state = state,
state = state, modifier = Modifier.align(Alignment.TopCenter)
modifier = Modifier.align(Alignment.TopCenter)
)
}
}
}
}
@Composable
fun RoomCard(
room: RoomEntity,
onRoomClick: (RoomEntity) -> Unit = {},
modifier: Modifier = Modifier
) {
val AppColors = LocalAppTheme.current
val context = LocalContext.current
val roomDebouncer = rememberDebouncer()
val cardSize = 180.dp
// 构建头像URL
val avatarUrl = if (room.avatar.isNotEmpty()) {
"${ConstVars.BASE_SERVER}/api/v1/outside/${room.avatar}?token=${AppStore.token}"
} else {
// 如果头像为空,使用群头像接口
val groupIdBase64 = Base64.encodeToString(
room.trtcType.toByteArray(),
Base64.NO_WRAP
)
"${ApiClient.RETROFIT_URL}group/avatar?groupIdBase64=${groupIdBase64}&token=${AppStore.token}"
}
// 优先显示cover如果没有cover则显示recommendBanner最后显示avatar
val imageUrl = when {
room.cover.isNotEmpty() -> "${ConstVars.BASE_SERVER}/api/v1/outside/${room.cover}?token=${AppStore.token}"
room.recommendBanner.isNotEmpty() -> "${ConstVars.BASE_SERVER}/api/v1/outside/${room.recommendBanner}?token=${AppStore.token}"
else -> avatarUrl
}
// 正方形卡片,文字重叠在底部
Box(
modifier = modifier
.size(cardSize)
.background(AppColors.tabUnselectedBackground, RoundedCornerShape(12.dp))
.noRippleClickable {
roomDebouncer {
onRoomClick(room)
}
}
) {
CustomAsyncImage(
context = context,
imageUrl = imageUrl,
contentDescription = room.name,
modifier = Modifier
.width(cardSize)
.height(120.dp)
.clip(RoundedCornerShape(
topStart = 12.dp,
topEnd = 12.dp,
bottomStart = 0.dp,
bottomEnd = 0.dp)),
contentScale = ContentScale.Crop,
defaultRes = R.mipmap.rider_pro_agent
)
// 房间名称,重叠在底部
Box(
modifier = Modifier
.align(Alignment.BottomCenter)
.fillMaxWidth()
.padding(bottom = 32.dp, start = 10.dp, end = 10.dp)
.clip(RoundedCornerShape(12.dp))
) {
Text(
text = room.name,
fontSize = 14.sp,
fontWeight = FontWeight.W900,
color = AppColors.text,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
modifier = Modifier.fillMaxWidth(),
textAlign = TextAlign.Left
) )
} }
// 显示人数
Box(
modifier = Modifier
.align(Alignment.BottomCenter)
.fillMaxWidth()
.padding(bottom = 10.dp, start = 10.dp, end = 10.dp)
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxWidth()
) {
Image(
painter = painterResource(R.drawable.rider_pro_nav_profile),
contentDescription = "chat",
modifier = Modifier.size(16.dp),
colorFilter = ColorFilter.tint(AppColors.secondaryText)
)
Spacer(modifier = Modifier.width(4.dp))
Text(
text = "${room.userCount} ${stringResource(R.string.chatting_now)}",
fontSize = 12.sp,
modifier = Modifier
.alpha(0.6f)
.weight(1f),
color = AppColors.text,
fontWeight = FontWeight.W500,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
}
} }
} }
/**
* 列表样式的房间项,供搜索等场景复用
*/
@Composable @Composable
fun RoomItem( fun RoomItem(
room: RoomEntity, room: RoomEntity,
@@ -380,6 +230,7 @@ fun RoomItem(
val avatarUrl = if (room.avatar.isNotEmpty()) { val avatarUrl = if (room.avatar.isNotEmpty()) {
"${ConstVars.BASE_SERVER}/api/v1/outside/${room.avatar}?token=${AppStore.token}" "${ConstVars.BASE_SERVER}/api/v1/outside/${room.avatar}?token=${AppStore.token}"
} else { } else {
// 如果头像为空,使用群头像接口
val groupIdBase64 = Base64.encodeToString( val groupIdBase64 = Base64.encodeToString(
room.trtcType.toByteArray(), room.trtcType.toByteArray(),
Base64.NO_WRAP Base64.NO_WRAP
@@ -456,7 +307,7 @@ fun RoomItem(
} }
@Composable @Composable
fun SegmentedControl( private fun SegmentedControl(
selectedIndex: Int, selectedIndex: Int,
onSegmentSelected: (Int) -> Unit, onSegmentSelected: (Int) -> Unit,
modifier: Modifier = Modifier modifier: Modifier = Modifier
@@ -521,7 +372,7 @@ private fun SegmentButton(
}, },
shape = RoundedCornerShape(1000.dp) shape = RoundedCornerShape(1000.dp)
) )
.noRippleClickable { onClick() }, .clickable(onClick = onClick),
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ) {
Text( Text(
@@ -538,43 +389,14 @@ private fun SegmentButton(
} }
@Composable @Composable
private fun EmptyStateIllustration( private fun EmptyStateIllustration() {
isNetworkAvailable: Boolean, Image(
onReload: () -> Unit painter = painterResource(id = R.mipmap.l_empty_img),
) { contentDescription = "空状态",
val AppColors = LocalAppTheme.current modifier = Modifier
if (isNetworkAvailable) { .width(181.dp)
EmptyStateView( .height(153.dp),
contentDescription = "空状态", contentScale = ContentScale.Fit
fontWeight = FontWeight.SemiBold )
)
} else {
Image(
painter = painterResource(id = R.mipmap.invalid_name_10),
contentDescription = "network error",
modifier = Modifier.size(181.dp),
)
Spacer(modifier = Modifier.height(24.dp))
Text(
text = stringResource(R.string.friend_chat_no_network_title),
fontSize = 16.sp,
color = AppColors.text,
fontWeight = FontWeight.W600
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = stringResource(R.string.friend_chat_no_network_subtitle),
fontSize = 14.sp,
color = AppColors.secondaryText,
fontWeight = FontWeight.W400
)
Spacer(modifier = Modifier.height(16.dp))
ReloadButton(onClick = onReload)
}
} }

View File

@@ -29,12 +29,9 @@ 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
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.layout.positionInRoot
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
@@ -55,7 +52,6 @@ import com.aiosman.ravenow.ui.NavigationRoute
import com.aiosman.ravenow.entity.AgentEntity import com.aiosman.ravenow.entity.AgentEntity
import com.aiosman.ravenow.ui.composables.CustomAsyncImage import com.aiosman.ravenow.ui.composables.CustomAsyncImage
import com.aiosman.ravenow.ui.index.tabs.profile.MyProfileViewModel import com.aiosman.ravenow.ui.index.tabs.profile.MyProfileViewModel
import com.aiosman.ravenow.ui.modifiers.noRippleClickable
import com.aiosman.ravenow.ui.network.ReloadButton import com.aiosman.ravenow.ui.network.ReloadButton
import com.aiosman.ravenow.utils.DebounceUtils import com.aiosman.ravenow.utils.DebounceUtils
import com.aiosman.ravenow.utils.NetworkUtils import com.aiosman.ravenow.utils.NetworkUtils
@@ -70,13 +66,7 @@ fun UserAgentsList(
showNoMoreText: Boolean = false, showNoMoreText: Boolean = false,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
state: LazyListState, state: LazyListState,
nestedScrollConnection: NestedScrollConnection? = null, nestedScrollConnection: NestedScrollConnection? = null
showSegments: Boolean = true, // 是否显示分段控制器(全部、公开、私有)
segmentSelectedIndex: Int = 0,
onSegmentSelected: (Int) -> Unit = {},
onSegmentMeasured: ((Float, Int) -> Unit)? = null,
isSegmentSticky: Boolean = false,
parentScrollProvider: () -> Int = { 0 }
) { ) {
val AppColors = LocalAppTheme.current val AppColors = LocalAppTheme.current
val listModifier = if (nestedScrollConnection != null) { val listModifier = if (nestedScrollConnection != null) {
@@ -90,14 +80,7 @@ fun UserAgentsList(
Box( Box(
modifier = listModifier.fillMaxSize() modifier = listModifier.fillMaxSize()
) { ) {
AgentEmptyContentWithSegments( AgentEmptyContentWithSegments()
showSegments = showSegments,
segmentSelectedIndex = segmentSelectedIndex,
onSegmentSelected = onSegmentSelected,
onSegmentMeasured = onSegmentMeasured,
isSegmentSticky = isSegmentSticky,
parentScrollProvider = parentScrollProvider
)
} }
} else { } else {
LazyColumn( LazyColumn(
@@ -265,14 +248,8 @@ fun UserAgentCard(
} }
@Composable @Composable
fun AgentEmptyContentWithSegments( fun AgentEmptyContentWithSegments() {
showSegments: Boolean = true, var selectedSegment by remember { mutableStateOf(0) } // 0: 全部, 1: 公开, 2: 私有
segmentSelectedIndex: Int = 0,
onSegmentSelected: (Int) -> Unit = {},
onSegmentMeasured: ((Float, Int) -> Unit)? = null,
isSegmentSticky: Boolean = false,
parentScrollProvider: () -> Int = { 0 }
) {
val AppColors = LocalAppTheme.current val AppColors = LocalAppTheme.current
val isNetworkAvailable = NetworkUtils.isNetworkAvailable(LocalContext.current) val isNetworkAvailable = NetworkUtils.isNetworkAvailable(LocalContext.current)
@@ -283,24 +260,14 @@ fun AgentEmptyContentWithSegments(
) { ) {
Spacer(modifier = Modifier.height(16.dp)) Spacer(modifier = Modifier.height(16.dp))
// 只在查看自己的智能体时显示分段控制器 // 分段控制器
if (showSegments) { AgentSegmentedControl(
AgentSegmentedControl( selectedIndex = selectedSegment,
selectedIndex = segmentSelectedIndex, onSegmentSelected = { selectedSegment = it },
onSegmentSelected = onSegmentSelected, modifier = Modifier.fillMaxWidth()
modifier = Modifier )
.fillMaxWidth()
.onGloballyPositioned { coordinates ->
onSegmentMeasured?.invoke(
coordinates.positionInRoot().y + parentScrollProvider(),
coordinates.size.height
)
}
.alpha(if (isSegmentSticky) 0f else 1f)
)
Spacer(modifier = Modifier.height(8.dp)) Spacer(modifier = Modifier.height(8.dp))
}
// 空状态内容(与动态、群聊保持一致) // 空状态内容(与动态、群聊保持一致)
Column( Column(
@@ -308,8 +275,25 @@ fun AgentEmptyContentWithSegments(
horizontalAlignment = Alignment.CenterHorizontally horizontalAlignment = Alignment.CenterHorizontally
) { ) {
if (isNetworkAvailable) { if (isNetworkAvailable) {
EmptyStateView( Image(
contentDescription = "暂无Agent" painter = painterResource(id = R.mipmap.l_empty_img),
contentDescription = "暂无Agent",
modifier = Modifier
.size(width = 181.dp, height = 153.dp)
.align(Alignment.CenterHorizontally),
)
Spacer(modifier = Modifier.height(9.dp))
Text(
text = stringResource(R.string.cosmos_awaits),
fontSize = 16.sp,
color = AppColors.text,
fontWeight = FontWeight.W600,
textAlign = TextAlign.Center,
modifier = Modifier.padding(horizontal = 24.dp),
maxLines = 2,
overflow = TextOverflow.Ellipsis
) )
} else { } else {
Image( Image(
@@ -348,7 +332,7 @@ fun AgentEmptyContentWithSegments(
} }
@Composable @Composable
fun AgentSegmentedControl( private fun AgentSegmentedControl(
selectedIndex: Int, selectedIndex: Int,
onSegmentSelected: (Int) -> Unit, onSegmentSelected: (Int) -> Unit,
modifier: Modifier = Modifier modifier: Modifier = Modifier
@@ -413,7 +397,7 @@ private fun AgentSegmentButton(
}, },
shape = RoundedCornerShape(1000.dp) shape = RoundedCornerShape(1000.dp)
) )
.noRippleClickable { onClick() }, .clickable(onClick = onClick),
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ) {
Text( Text(

View File

@@ -84,7 +84,6 @@ import com.aiosman.ravenow.ui.NavigationRoute
import com.google.accompanist.systemuicontroller.rememberSystemUiController import com.google.accompanist.systemuicontroller.rememberSystemUiController
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import com.aiosman.ravenow.utils.NetworkUtils import com.aiosman.ravenow.utils.NetworkUtils
import com.aiosman.ravenow.ui.network.NetworkErrorContentInline
@OptIn(ExperimentalFoundationApi::class) @OptIn(ExperimentalFoundationApi::class)
@@ -416,17 +415,64 @@ fun MomentResultTab() {
.background(AppColors.background) .background(AppColors.background)
) { ) {
if (moments.itemCount == 0 && model.showResult) { if (moments.itemCount == 0 && model.showResult) {
val isNetworkAvailable = NetworkUtils.isNetworkAvailable(context) Column(
SearchPlaceholderContent(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.padding(16.dp), .padding(16.dp),
isNetworkAvailable = isNetworkAvailable, horizontalAlignment = Alignment.CenterHorizontally,
onReload = { verticalArrangement = Arrangement.Center
SearchViewModel.ResetModel() ) {
SearchViewModel.search() val isNetworkAvailable = NetworkUtils.isNetworkAvailable(context)
if (isNetworkAvailable) {
androidx.compose.foundation.Image(
painter = painterResource(
id = if(AppState.darkMode) R.mipmap.syss_yh_qs_as_img
else R.mipmap.invalid_name_1),
contentDescription = "No Comment",
modifier = Modifier.size(140.dp)
)
Text(
text = "咦,什么都没找到...",
color = LocalAppTheme.current.text,
fontSize = 16.sp,
fontWeight = FontWeight.W600
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = "换个关键词试试吧,也许会有新发现!",
color = LocalAppTheme.current.secondaryText,
fontSize = 14.sp,
fontWeight = FontWeight.W400
)
} else {
androidx.compose.foundation.Image(
painter = painterResource(id = R.mipmap.invalid_name_10),
contentDescription = "network error",
modifier = Modifier.size(140.dp)
)
Text(
text = stringResource(R.string.friend_chat_no_network_title),
color = LocalAppTheme.current.text,
fontSize = 16.sp,
fontWeight = FontWeight.W600
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = stringResource(R.string.friend_chat_no_network_subtitle),
color = LocalAppTheme.current.secondaryText,
fontSize = 14.sp,
fontWeight = FontWeight.W400
)
Spacer(modifier = Modifier.size(16.dp))
ReloadButton(
onClick = {
SearchViewModel.ResetModel()
SearchViewModel.search()
}
)
} }
) }
} else { } else {
LazyColumn( LazyColumn(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
@@ -520,17 +566,64 @@ fun UserResultTab() {
modifier = Modifier.fillMaxSize() modifier = Modifier.fillMaxSize()
) { ) {
if (users.itemCount == 0 && model.showResult) { if (users.itemCount == 0 && model.showResult) {
val isNetworkAvailable = NetworkUtils.isNetworkAvailable(context) Column(
SearchPlaceholderContent(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.padding(16.dp), .padding(16.dp),
isNetworkAvailable = isNetworkAvailable, horizontalAlignment = Alignment.CenterHorizontally,
onReload = { verticalArrangement = Arrangement.Center
SearchViewModel.ResetModel() ) {
SearchViewModel.search() val isNetworkAvailable = NetworkUtils.isNetworkAvailable(context)
if (isNetworkAvailable) {
androidx.compose.foundation.Image(
painter = painterResource(
id = if(AppState.darkMode) R.mipmap.syss_yh_qs_as_img
else R.mipmap.invalid_name_1),
contentDescription = "No Comment",
modifier = Modifier.size(140.dp)
)
Text(
text = "咦,什么都没找到...",
color = LocalAppTheme.current.text,
fontSize = 16.sp,
fontWeight = FontWeight.W600
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = "换个关键词试试吧,也许会有新发现!",
color = LocalAppTheme.current.secondaryText,
fontSize = 14.sp,
fontWeight = FontWeight.W400
)
} else {
androidx.compose.foundation.Image(
painter = painterResource(id = R.mipmap.invalid_name_10),
contentDescription = "network error",
modifier = Modifier.size(140.dp)
)
Text(
text = stringResource(R.string.friend_chat_no_network_title),
color = LocalAppTheme.current.text,
fontSize = 16.sp,
fontWeight = FontWeight.W600
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = stringResource(R.string.friend_chat_no_network_subtitle),
color = LocalAppTheme.current.secondaryText,
fontSize = 14.sp,
fontWeight = FontWeight.W400
)
Spacer(modifier = Modifier.size(16.dp))
ReloadButton(
onClick = {
SearchViewModel.ResetModel()
SearchViewModel.search()
}
)
} }
) }
} else { } else {
LazyColumn( LazyColumn(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
@@ -641,17 +734,64 @@ fun AiResultTab() {
.background(AppColors.background) .background(AppColors.background)
) { ) {
if (agents.itemCount == 0 && model.showResult) { if (agents.itemCount == 0 && model.showResult) {
val isNetworkAvailable = NetworkUtils.isNetworkAvailable(context) Column(
SearchPlaceholderContent(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.padding(16.dp), .padding(16.dp),
isNetworkAvailable = isNetworkAvailable, horizontalAlignment = Alignment.CenterHorizontally,
onReload = { verticalArrangement = Arrangement.Center
SearchViewModel.ResetModel() ) {
SearchViewModel.search() val isNetworkAvailable = NetworkUtils.isNetworkAvailable(context)
if (isNetworkAvailable) {
androidx.compose.foundation.Image(
painter = painterResource(
id = if (AppState.darkMode) R.mipmap.syss_yh_qs_as_img
else R.mipmap.invalid_name_1
),
contentDescription = "No Result",
modifier = Modifier.size(140.dp)
)
Text(
text = "咦,什么都没找到...",
color = LocalAppTheme.current.text,
fontSize = 16.sp,
fontWeight = FontWeight.W600
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = "换个关键词试试吧,也许会有新发现!",
color = LocalAppTheme.current.secondaryText,
fontSize = 14.sp,
fontWeight = FontWeight.W400
)
} else {
androidx.compose.foundation.Image(
painter = painterResource(id = R.mipmap.invalid_name_10),
contentDescription = "network error",
modifier = Modifier.size(140.dp)
)
Text(
text = stringResource(R.string.friend_chat_no_network_title),
color = LocalAppTheme.current.text,
fontSize = 16.sp,
fontWeight = FontWeight.W600
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = stringResource(R.string.friend_chat_no_network_subtitle),
color = LocalAppTheme.current.secondaryText,
fontSize = 14.sp,
fontWeight = FontWeight.W400
)
Spacer(modifier = Modifier.size(16.dp))
ReloadButton(
onClick = {
SearchViewModel.ResetModel()
SearchViewModel.search()
}
)
} }
) }
} else { } else {
LazyColumn( LazyColumn(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
@@ -723,17 +863,65 @@ fun RoomResultTab() {
.background(AppColors.background) .background(AppColors.background)
) { ) {
if (rooms.itemCount == 0 && model.showResult) { if (rooms.itemCount == 0 && model.showResult) {
val isNetworkAvailable = NetworkUtils.isNetworkAvailable(context) Column(
SearchPlaceholderContent(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.padding(16.dp), .padding(16.dp),
isNetworkAvailable = isNetworkAvailable, horizontalAlignment = Alignment.CenterHorizontally,
onReload = { verticalArrangement = Arrangement.Center
SearchViewModel.ResetModel() ) {
SearchViewModel.search() val isNetworkAvailable = NetworkUtils.isNetworkAvailable(context)
if (isNetworkAvailable) {
androidx.compose.foundation.Image(
painter = painterResource(
id = if (AppState.darkMode) R.mipmap.syss_yh_qs_as_img
else R.mipmap.invalid_name_1
),
contentDescription = "No Result",
modifier = Modifier.size(140.dp)
)
Text(
text = "咦,什么都没找到...",
color = LocalAppTheme.current.text,
fontSize = 16.sp,
fontWeight = FontWeight.W600
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = "换个关键词试试吧,也许会有新发现!",
color = LocalAppTheme.current.secondaryText,
fontSize = 14.sp,
fontWeight = FontWeight.W400
)
} else {
androidx.compose.foundation.Image(
painter = painterResource(id = R.mipmap.invalid_name_10),
contentDescription = "network error",
modifier = Modifier.size(140.dp)
)
Text(
text = stringResource(R.string.friend_chat_no_network_title),
color = LocalAppTheme.current.text,
fontSize = 16.sp,
fontWeight = FontWeight.W600
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = stringResource(R.string.friend_chat_no_network_subtitle),
color = LocalAppTheme.current.secondaryText,
fontSize = 14.sp,
fontWeight = FontWeight.W400
)
Spacer(modifier = Modifier.size(16.dp))
ReloadButton(
onClick = {
SearchViewModel.ResetModel()
SearchViewModel.search()
}
)
} }
) }
} else { } else {
LazyColumn( LazyColumn(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
@@ -757,32 +945,41 @@ fun RoomResultTab() {
} }
@Composable @Composable
fun SearchPlaceholderContent( fun ReloadButton(
modifier: Modifier = Modifier, onClick: () -> Unit
isNetworkAvailable: Boolean,
onReload: () -> Unit
) { ) {
val appColors = LocalAppTheme.current val gradientBrush = Brush.linearGradient(
Column( colors = listOf(
modifier = modifier, Color(0xFF7c45ed),
horizontalAlignment = Alignment.CenterHorizontally, Color(0xFF7c68ef),
verticalArrangement = Arrangement.Center Color(0xFF7bd8f8)
)
)
Button(
onClick = onClick,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 120.dp)
.height(48.dp),
shape = RoundedCornerShape(30.dp),
colors = ButtonDefaults.buttonColors(
backgroundColor = Color.Transparent
),
contentPadding = PaddingValues(0.dp)
) { ) {
if (isNetworkAvailable) { Box(
androidx.compose.foundation.Image( modifier = Modifier
painter = painterResource(id = R.mipmap.empty_img), .fillMaxSize()
contentDescription = "No Comment", .background(gradientBrush),
modifier = Modifier.size(168.dp) contentAlignment = Alignment.Center
) ) {
Text( Text(
text = stringResource(R.string.null_search), text = stringResource(R.string.Reload),
color = appColors.text,
fontSize = 16.sp, fontSize = 16.sp,
fontWeight = FontWeight.W600 fontWeight = FontWeight.Bold,
color = Color.White,
) )
} else {
NetworkErrorContentInline(onReload = onReload)
} }
} }
} }

View File

@@ -19,8 +19,6 @@ import com.aiosman.ravenow.entity.MomentEntity
import com.aiosman.ravenow.entity.MomentPagingSource import com.aiosman.ravenow.entity.MomentPagingSource
import com.aiosman.ravenow.entity.MomentRemoteDataSource import com.aiosman.ravenow.entity.MomentRemoteDataSource
import com.aiosman.ravenow.entity.MomentServiceImpl import com.aiosman.ravenow.entity.MomentServiceImpl
import com.aiosman.ravenow.event.MomentFavouriteChangeEvent
import com.aiosman.ravenow.event.MomentLikeChangeEvent
import com.aiosman.ravenow.entity.AgentEntity import com.aiosman.ravenow.entity.AgentEntity
import com.aiosman.ravenow.entity.AgentRemoteDataSource import com.aiosman.ravenow.entity.AgentRemoteDataSource
import com.aiosman.ravenow.entity.AgentSearchPagingSource import com.aiosman.ravenow.entity.AgentSearchPagingSource
@@ -33,7 +31,6 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.greenrobot.eventbus.EventBus
object SearchViewModel : ViewModel() { object SearchViewModel : ViewModel() {
var searchText by mutableStateOf("") var searchText by mutableStateOf("")
@@ -207,14 +204,7 @@ object SearchViewModel : ViewModel() {
suspend fun likeMoment(id: Int) { suspend fun likeMoment(id: Int) {
try { try {
momentService.likeMoment(id) momentService.likeMoment(id)
val likeCount = updateMomentLike(id, true) updateMomentLike(id, true)
EventBus.getDefault().post(
MomentLikeChangeEvent(
postId = id,
likeCount = likeCount,
isLike = true
)
)
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() e.printStackTrace()
} }
@@ -223,14 +213,7 @@ object SearchViewModel : ViewModel() {
suspend fun dislikeMoment(id: Int) { suspend fun dislikeMoment(id: Int) {
try { try {
momentService.dislikeMoment(id) momentService.dislikeMoment(id)
val likeCount = updateMomentLike(id, false) updateMomentLike(id, false)
EventBus.getDefault().post(
MomentLikeChangeEvent(
postId = id,
likeCount = likeCount,
isLike = false
)
)
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() e.printStackTrace()
} }
@@ -240,12 +223,6 @@ object SearchViewModel : ViewModel() {
try { try {
momentService.favoriteMoment(id) momentService.favoriteMoment(id)
updateMomentFavorite(id, true) updateMomentFavorite(id, true)
EventBus.getDefault().post(
MomentFavouriteChangeEvent(
postId = id,
isFavourite = true
)
)
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() e.printStackTrace()
} }
@@ -255,12 +232,6 @@ object SearchViewModel : ViewModel() {
try { try {
momentService.unfavoriteMoment(id) momentService.unfavoriteMoment(id)
updateMomentFavorite(id, false) updateMomentFavorite(id, false)
EventBus.getDefault().post(
MomentFavouriteChangeEvent(
postId = id,
isFavourite = false
)
)
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() e.printStackTrace()
} }
@@ -270,23 +241,19 @@ object SearchViewModel : ViewModel() {
updateMomentCommentCount(id, 1) updateMomentCommentCount(id, 1)
} }
private fun updateMomentLike(id: Int, isLike: Boolean): Int? { private fun updateMomentLike(id: Int, isLike: Boolean) {
var latestLikeCount: Int? = null
val currentPagingData = _momentsFlow.value val currentPagingData = _momentsFlow.value
val updatedPagingData = currentPagingData.map { momentItem -> val updatedPagingData = currentPagingData.map { momentItem ->
if (momentItem.id == id) { if (momentItem.id == id) {
val nextCount = (momentItem.likeCount + if (isLike) 1 else -1).coerceAtLeast(0)
latestLikeCount = nextCount
momentItem.copy( momentItem.copy(
liked = isLike, liked = isLike,
likeCount = nextCount likeCount = momentItem.likeCount + if (isLike) 1 else -1
) )
} else { } else {
momentItem momentItem
} }
} }
_momentsFlow.value = updatedPagingData _momentsFlow.value = updatedPagingData
return latestLikeCount
} }
private fun updateMomentFavorite(id: Int, isFavorite: Boolean) { private fun updateMomentFavorite(id: Int, isFavorite: Boolean) {
@@ -295,7 +262,7 @@ object SearchViewModel : ViewModel() {
if (momentItem.id == id) { if (momentItem.id == id) {
momentItem.copy( momentItem.copy(
isFavorite = isFavorite, isFavorite = isFavorite,
favoriteCount = (momentItem.favoriteCount + if (isFavorite) 1 else -1).coerceAtLeast(0) favoriteCount = momentItem.favoriteCount + if (isFavorite) 1 else -1
) )
} else { } else {
momentItem momentItem

View File

@@ -13,7 +13,6 @@ import androidx.compose.foundation.background
import androidx.compose.foundation.border import androidx.compose.foundation.border
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.gestures.Orientation import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
@@ -26,7 +25,10 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.ui.graphics.RectangleShape import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.PlayArrow
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.material3.rememberModalBottomSheetState
@@ -54,7 +56,6 @@ import androidx.compose.ui.text.font.FontWeight
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.input.pointer.pointerInput
import androidx.compose.ui.viewinterop.AndroidView import androidx.compose.ui.viewinterop.AndroidView
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver import androidx.lifecycle.LifecycleEventObserver
@@ -318,9 +319,9 @@ private fun SingleVideoItemContent(
isPageVisible: Boolean = true isPageVisible: Boolean = true
) { ) {
// 将暂停状态移到每个视频项内部,使用 remember 保存,避免在点赞/关注时被重置 // 将暂停状态移到每个视频项内部,使用 remember 保存,避免在点赞/关注时被重置
val pauseIconVisibleState = remember(pager) { mutableStateOf(false) } val pauseIconVisibleState = remember(pager) {
// 记录进入后台前是否在播放,用于决定是否需要自动恢复播放 mutableStateOf(false)
val shouldResumeAfterLifecyclePause = remember(pager) { mutableStateOf(false) } }
// 当页面切换时,重置暂停状态 // 当页面切换时,重置暂停状态
LaunchedEffect(pager, pagerState.currentPage) { LaunchedEffect(pager, pagerState.currentPage) {
@@ -340,7 +341,6 @@ private fun SingleVideoItemContent(
pagerState = pagerState, pagerState = pagerState,
pager = pager, pager = pager,
pauseIconVisibleState = pauseIconVisibleState, pauseIconVisibleState = pauseIconVisibleState,
shouldResumeAfterLifecyclePause = shouldResumeAfterLifecyclePause,
onLikeClick = onLikeClick, onLikeClick = onLikeClick,
onCommentClick = onCommentClick, onCommentClick = onCommentClick,
onCommentAdded = onCommentAdded, onCommentAdded = onCommentAdded,
@@ -373,7 +373,6 @@ fun VideoPlayer(
pagerState: PagerState, pagerState: PagerState,
pager: Int, pager: Int,
pauseIconVisibleState: MutableState<Boolean>, pauseIconVisibleState: MutableState<Boolean>,
shouldResumeAfterLifecyclePause: MutableState<Boolean>,
onLikeClick: ((MomentEntity) -> Unit)? = null, onLikeClick: ((MomentEntity) -> Unit)? = null,
onCommentClick: ((MomentEntity) -> Unit)? = null, onCommentClick: ((MomentEntity) -> Unit)? = null,
onCommentAdded: ((MomentEntity) -> Unit)? = null, onCommentAdded: ((MomentEntity) -> Unit)? = null,
@@ -468,9 +467,6 @@ fun VideoPlayer(
.clip(RectangleShape) .clip(RectangleShape)
) { ) {
var playerView by remember { mutableStateOf<PlayerView?>(null) } var playerView by remember { mutableStateOf<PlayerView?>(null) }
// 防抖:记录上次双击时间,防止快速重复双击
val lastDoubleTapTime = remember { mutableStateOf(0L) }
val doubleTapDebounceTime = 500L // 500ms 防抖时间
// 使用 key 强制每个视频的 PlayerView 完全独立,避免布局状态残留 // 使用 key 强制每个视频的 PlayerView 完全独立,避免布局状态残留
androidx.compose.runtime.key(videoUrl) { androidx.compose.runtime.key(videoUrl) {
@@ -483,31 +479,15 @@ fun VideoPlayer(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.clip(RectangleShape) .clip(RectangleShape)
.pointerInput(videoUrl, moment?.liked) { .noRippleClickable {
detectTapGestures( handleVideoClick(pauseIconVisibleState, exoPlayer, scope)
onDoubleTap = { offset ->
// 双击点赞/取消点赞
val currentTime = System.currentTimeMillis()
if (currentTime - lastDoubleTapTime.value > doubleTapDebounceTime) {
lastDoubleTapTime.value = currentTime
moment?.let {
// 检查当前点赞状态,如果已点赞则取消点赞,如果未点赞则点赞
onLikeClick?.invoke(it)
}
}
},
onTap = {
// 单击播放/暂停
handleVideoClick(pauseIconVisibleState, exoPlayer, scope)
}
)
} }
) )
} }
if (pauseIconVisibleState.value) { if (pauseIconVisibleState.value) {
Image( Icon(
painter = painterResource(R.mipmap.dt_ts_sp_bf_btn), imageVector = Icons.Default.PlayArrow,
contentDescription = null, contentDescription = null,
modifier = Modifier modifier = Modifier
.align(Alignment.Center) .align(Alignment.Center)
@@ -534,26 +514,15 @@ fun VideoPlayer(
when (event) { when (event) {
Lifecycle.Event.ON_PAUSE -> { Lifecycle.Event.ON_PAUSE -> {
// 应用进入后台时暂停 // 应用进入后台时暂停
shouldResumeAfterLifecyclePause.value = exoPlayer.isPlaying && !pauseIconVisibleState.value
exoPlayer.playWhenReady = false exoPlayer.playWhenReady = false
exoPlayer.pause() exoPlayer.pause()
} }
Lifecycle.Event.ON_RESUME -> { Lifecycle.Event.ON_RESUME -> {
// 返回前台且为当前页面时恢复播放 // 返回前台且为当前页面时恢复播放
if ( if (pager == pagerState.currentPage) {
pager == pagerState.currentPage &&
isPageVisible &&
shouldResumeAfterLifecyclePause.value
) {
exoPlayer.playWhenReady = true exoPlayer.playWhenReady = true
exoPlayer.play() exoPlayer.play()
pauseIconVisibleState.value = false
} else {
// 未自动恢复播放时,如果当前页面视频处于暂停状态,则显示暂停图标
if (!exoPlayer.isPlaying) {
pauseIconVisibleState.value = true
}
} }
} }
@@ -691,8 +660,7 @@ fun VideoPlayer(
}, },
containerColor = AppColors.background, containerColor = AppColors.background,
sheetState = sheetState, sheetState = sheetState,
dragHandle = {}, shape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp)
shape = RoundedCornerShape(topStart = 28.dp, topEnd = 28.dp)
) { ) {
Box( Box(
modifier = Modifier modifier = Modifier
@@ -702,7 +670,6 @@ fun VideoPlayer(
CommentModalContent( CommentModalContent(
postId = moment.id, postId = moment.id,
commentCount = moment.commentCount, commentCount = moment.commentCount,
showTitle = false,
onCommentAdded = { onCommentAdded = {
onCommentAdded?.invoke(moment) onCommentAdded?.invoke(moment)
} }

View File

@@ -28,7 +28,6 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
@@ -47,10 +46,10 @@ import com.aiosman.ravenow.ui.modifiers.noRippleClickable
import com.aiosman.ravenow.ui.navigateToPost import com.aiosman.ravenow.ui.navigateToPost
import java.util.Date import java.util.Date
import com.aiosman.ravenow.utils.NetworkUtils import com.aiosman.ravenow.utils.NetworkUtils
import com.aiosman.ravenow.ui.network.NetworkErrorContent import com.aiosman.ravenow.ui.network.ReloadButton
@Preview @Preview
@Composable @Composable
fun LikeNoticeScreen(includeStatusBarPadding: Boolean = true) { fun LikeNoticeScreen() {
val model = LikeNoticeViewModel val model = LikeNoticeViewModel
val listState = rememberLazyListState() val listState = rememberLazyListState()
var dataFlow = model.likeItemsFlow var dataFlow = model.likeItemsFlow
@@ -64,8 +63,7 @@ fun LikeNoticeScreen(includeStatusBarPadding: Boolean = true) {
StatusBarMaskLayout( StatusBarMaskLayout(
darkIcons = !AppState.darkMode, darkIcons = !AppState.darkMode,
maskBoxBackgroundColor = AppColors.background, maskBoxBackgroundColor = AppColors.background
includeStatusBarPadding = includeStatusBarPadding
) { ) {
Column( Column(
modifier = Modifier modifier = Modifier
@@ -77,11 +75,42 @@ fun LikeNoticeScreen(includeStatusBarPadding: Boolean = true) {
val isNetworkAvailable = NetworkUtils.isNetworkAvailable(LocalContext.current) val isNetworkAvailable = NetworkUtils.isNetworkAvailable(LocalContext.current)
if (!isNetworkAvailable) { if (!isNetworkAvailable) {
NetworkErrorContent( Box(
onReload = { modifier = Modifier.fillMaxSize()
LikeNoticeViewModel.reload(force = true) .padding(top=149.dp),
contentAlignment = Alignment.TopCenter
) {
Column(
modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally
) {
Image(
painter = painterResource(id = R.mipmap.invalid_name_10),
contentDescription = "network error",
modifier = Modifier.size(181.dp)
)
Spacer(modifier = Modifier.height(24.dp))
Text(
text = stringResource(R.string.friend_chat_no_network_title),
color = AppColors.text,
fontSize = 16.sp,
fontWeight = FontWeight.W600,
)
Spacer(modifier = Modifier.size(8.dp))
Text(
text = stringResource(R.string.friend_chat_no_network_subtitle),
color = AppColors.text,
fontSize = 14.sp,
fontWeight = FontWeight.W400
)
Spacer(modifier = Modifier.height(16.dp))
ReloadButton(
onClick = {
LikeNoticeViewModel.reload(force = true)
}
)
} }
) }
} else if (likes.itemCount == 0) { } else if (likes.itemCount == 0) {
Box( Box(
modifier = Modifier.fillMaxSize() modifier = Modifier.fillMaxSize()
@@ -155,83 +184,57 @@ fun ActionPostNoticeItem(
val navController = LocalNavController.current val navController = LocalNavController.current
val AppColors = LocalAppTheme.current val AppColors = LocalAppTheme.current
val actionLabel = when (action) { Box(
"favourite" -> stringResource(R.string.favourite_your_post) modifier = Modifier.padding(vertical = 16.dp)
else -> stringResource(R.string.like_your_post)
}
Row(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 12.dp, horizontal = 0.dp),
verticalAlignment = Alignment.CenterVertically
) { ) {
CustomAsyncImage(
context = context,
imageUrl = avatar,
modifier = Modifier
.size(40.dp)
.clip(CircleShape)
.noRippleClickable {
navController.navigate(
NavigationRoute.AccountProfile.route.replace(
"{id}",
userId.toString()
)
)
},
contentDescription = action,
)
Row( Row(
modifier = Modifier modifier = Modifier.fillMaxWidth(),
.weight(1f) verticalAlignment = Alignment.Top,
.padding(start = 8.dp)
.noRippleClickable {
navController.navigateToPost(
id = postId,
highlightCommentId = 0,
initImagePagerIndex = 0
)
},
verticalAlignment = Alignment.CenterVertically
) { ) {
CustomAsyncImage(
context,
imageUrl = avatar,
modifier = Modifier
.size(48.dp)
.clip(CircleShape)
.noRippleClickable {
navController.navigate(
NavigationRoute.AccountProfile.route.replace(
"{id}",
userId.toString()
)
)
},
contentDescription = action,
)
Spacer(modifier = Modifier.width(12.dp))
Column( Column(
modifier = Modifier.weight(1f) modifier = Modifier
.weight(1f)
.noRippleClickable {
navController.navigateToPost(
id = postId,
highlightCommentId = 0,
initImagePagerIndex = 0
)
}
) { ) {
Text( Text(nickName, fontWeight = FontWeight.Bold, fontSize = 16.sp, color = AppColors.text)
text = nickName, Spacer(modifier = Modifier.height(2.dp))
fontWeight = FontWeight.Bold, when (action) {
fontSize = 14.sp, "like" -> Text(stringResource(R.string.like_your_post), color = AppColors.text)
color = AppColors.text, "favourite" -> Text(stringResource(R.string.favourite_your_post), color = AppColors.text)
maxLines = 1, }
overflow = TextOverflow.Ellipsis Spacer(modifier = Modifier.height(2.dp))
) Row {
Spacer(modifier = Modifier.height(4.dp)) Text(likeTime.timeAgo(context), fontSize = 12.sp, color = AppColors.secondaryText)
Row(
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = actionLabel,
fontSize = 14.sp,
color = AppColors.secondaryText,
modifier = Modifier.weight(1f),
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
Spacer(modifier = Modifier.width(4.dp))
Text(
text = likeTime.timeAgo(context),
fontSize = 14.sp,
color = AppColors.secondaryText
)
} }
} }
Spacer(modifier = Modifier.width(4.dp))
CustomAsyncImage( CustomAsyncImage(
context = context, context,
imageUrl = thumbnail, imageUrl = thumbnail,
modifier = Modifier modifier = Modifier
.size(40.dp) .size(48.dp)
.clip(RoundedCornerShape(8.dp)), .clip(RoundedCornerShape(8.dp)),
contentDescription = action, contentDescription = action,
) )
@@ -246,11 +249,10 @@ fun LikeCommentNoticeItem(
val navController = LocalNavController.current val navController = LocalNavController.current
val context = LocalContext.current val context = LocalContext.current
val AppColors = LocalAppTheme.current val AppColors = LocalAppTheme.current
val previewPost = item.comment?.replyComment?.post ?: item.comment?.post
Column( Box(
modifier = Modifier modifier = Modifier
.padding(vertical = 12.dp) .padding(vertical = 16.dp)
.noRippleClickable { .noRippleClickable {
item.comment?.postId.let { item.comment?.postId.let {
navController.navigateToPost( navController.navigateToPost(
@@ -261,103 +263,105 @@ fun LikeCommentNoticeItem(
} }
} }
) { ) {
Row( Row {
verticalAlignment = Alignment.CenterVertically Column(
) { modifier = Modifier.weight(1f)
CustomAsyncImage(
imageUrl = item.user.avatar,
modifier = Modifier
.size(40.dp)
.clip(CircleShape),
contentDescription = stringResource(R.string.like_your_comment)
)
Row(
modifier = Modifier
.weight(1f)
.padding(start = 8.dp),
verticalAlignment = Alignment.CenterVertically
) { ) {
Column( Row(
modifier = Modifier.weight(1f) modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.Top,
) { ) {
Text( CustomAsyncImage(
text = item.user.nickName, imageUrl = item.user.avatar,
fontWeight = FontWeight.Bold, modifier = Modifier
fontSize = 14.sp, .size(48.dp)
color = AppColors.text, .clip(CircleShape),
maxLines = 1, contentDescription = stringResource(R.string.like_your_comment)
overflow = TextOverflow.Ellipsis
) )
Spacer(modifier = Modifier.height(4.dp)) Spacer(modifier = Modifier.width(12.dp))
Row( Column(
verticalAlignment = Alignment.CenterVertically modifier = Modifier
.weight(1f)
) {
Text(item.user.nickName, fontWeight = FontWeight.Bold, fontSize = 16.sp, color = AppColors.text)
Spacer(modifier = Modifier.height(2.dp))
Text(stringResource(R.string.like_your_comment), color = AppColors.text)
Spacer(modifier = Modifier.height(2.dp))
Row {
Text(
item.likeTime.timeAgo(context),
fontSize = 12.sp,
color = AppColors.secondaryText
)
}
}
}
Spacer(modifier = Modifier.height(12.dp))
Row(
modifier = Modifier.padding(start = 48.dp)
) {
Box(
modifier = Modifier
.size(24.dp)
.background(Color.Gray.copy(alpha = 0.1f))
) {
CustomAsyncImage(
context = context,
imageUrl = AppState.profile?.avatar ?: "",
contentDescription = "Comment Profile Picture",
modifier = Modifier
.size(24.dp)
.clip(RoundedCornerShape(24.dp)),
contentScale = ContentScale.Crop
)
}
Spacer(modifier = Modifier.width(8.dp))
Column(
modifier = Modifier.weight(1f)
) { ) {
Text( Text(
text = stringResource(R.string.like_your_comment), text = AppState.profile?.nickName ?: "",
fontWeight = FontWeight.W600,
fontSize = 14.sp, fontSize = 14.sp,
color = AppColors.secondaryText, color = AppColors.text
modifier = Modifier.weight(1f),
maxLines = 1,
overflow = TextOverflow.Ellipsis
) )
Spacer(modifier = Modifier.width(4.dp))
Text( Text(
text = item.likeTime.timeAgo(context), text = item.comment?.content ?: "",
fontSize = 14.sp, fontSize = 12.sp,
color = AppColors.secondaryText color = AppColors.secondaryText,
maxLines = 2
) )
} }
} }
Spacer(modifier = Modifier.width(4.dp)) }
previewPost?.let { Spacer(modifier = Modifier.width(16.dp))
if (item.comment?.replyComment?.post != null) {
item.comment.replyComment.post.let {
CustomAsyncImage( CustomAsyncImage(
context = context, context = context,
imageUrl = it.images[0].thumbnail, imageUrl = it.images[0].thumbnail,
contentDescription = "Post Thumbnail", contentDescription = "Post Thumbnail",
modifier = Modifier modifier = Modifier
.size(40.dp) .size(48.dp)
.clip(RoundedCornerShape(8.dp)),
contentScale = ContentScale.Crop
)
}
} else {
item.comment?.post?.let {
CustomAsyncImage(
context = context,
imageUrl = it.images[0].thumbnail,
contentDescription = "Post Thumbnail",
modifier = Modifier
.size(48.dp)
.clip(RoundedCornerShape(8.dp)), .clip(RoundedCornerShape(8.dp)),
contentScale = ContentScale.Crop contentScale = ContentScale.Crop
) )
} }
} }
}
Spacer(modifier = Modifier.height(8.dp))
Row(
modifier = Modifier.padding(start = 48.dp)
) {
Box(
modifier = Modifier
.size(24.dp)
.background(Color.Gray.copy(alpha = 0.1f))
) {
CustomAsyncImage(
context = context,
imageUrl = AppState.profile?.avatar ?: "",
contentDescription = "Comment Profile Picture",
modifier = Modifier
.size(24.dp)
.clip(RoundedCornerShape(24.dp)),
contentScale = ContentScale.Crop
)
}
Spacer(modifier = Modifier.width(8.dp))
Column(
modifier = Modifier.weight(1f)
) {
Text(
text = AppState.profile?.nickName ?: "",
fontWeight = FontWeight.W600,
fontSize = 14.sp,
color = AppColors.text
)
Text(
text = item.comment?.content ?: "",
fontSize = 12.sp,
color = AppColors.secondaryText,
maxLines = 2
)
}
} }
} }
} }

View File

@@ -1,153 +0,0 @@
package com.aiosman.ravenow.ui.network
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
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 com.aiosman.ravenow.LocalAppTheme
import com.aiosman.ravenow.R
//关注、粉丝、通知、like界面网络缺省图
@Composable
fun NetworkErrorContent(
onReload: () -> Unit,
modifier: Modifier = Modifier
) {
val AppColors = LocalAppTheme.current
Box(
modifier = modifier
.fillMaxSize()
.padding(top = 149.dp),
contentAlignment = Alignment.TopCenter
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.fillMaxWidth()
) {
Image(
painter = painterResource(id = R.mipmap.invalid_name_10),
contentDescription = "network error",
modifier = Modifier.size(181.dp)
)
Spacer(modifier = Modifier.size(24.dp))
Text(
text = stringResource(R.string.friend_chat_no_network_title),
color = AppColors.text,
fontSize = 16.sp,
fontWeight = FontWeight.W600
)
Spacer(modifier = Modifier.size(8.dp))
Text(
text = stringResource(R.string.friend_chat_no_network_subtitle),
color = AppColors.text,
fontSize = 14.sp,
fontWeight = FontWeight.W400
)
Spacer(modifier = Modifier.height(16.dp))
ReloadButton(
onClick = onReload
)
}
}
}
@Composable
fun NetworkErrorContentInline(
onReload: () -> Unit,
modifier: Modifier = Modifier
) {
val AppColors = LocalAppTheme.current
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = modifier.fillMaxWidth()
) {
Image(
painter = painterResource(id = R.mipmap.invalid_name_10),
contentDescription = "network error",
modifier = Modifier.size(140.dp)
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = stringResource(R.string.friend_chat_no_network_title),
color = AppColors.text,
fontSize = 16.sp,
fontWeight = FontWeight.W600
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = stringResource(R.string.friend_chat_no_network_subtitle),
color = AppColors.secondaryText,
fontSize = 14.sp,
fontWeight = FontWeight.W400
)
Spacer(modifier = Modifier.size(16.dp))
ReloadButton(
onClick = onReload
)
}
}
//消息界面网络缺省图
@Composable
fun NetworkErrorContentCompact(
onReload: () -> Unit,
modifier: Modifier = Modifier
) {
val AppColors = LocalAppTheme.current
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = modifier
) {
Image(
painter = painterResource(id = R.mipmap.invalid_name_10),
contentDescription = "network error",
modifier = Modifier.size(120.dp)
)
Spacer(modifier = Modifier.height(24.dp))
Text(
text = stringResource(R.string.friend_chat_no_network_title),
color = AppColors.text,
fontSize = 16.sp,
fontWeight = FontWeight.W600,
textAlign = TextAlign.Center,
modifier = Modifier.padding(horizontal = 24.dp),
maxLines = 2,
overflow = TextOverflow.Ellipsis
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = stringResource(R.string.friend_chat_no_network_subtitle),
color = AppColors.secondaryText,
fontSize = 14.sp,
textAlign = TextAlign.Center,
modifier = Modifier.padding(horizontal = 24.dp),
maxLines = 3,
overflow = TextOverflow.Ellipsis
)
Spacer(modifier = Modifier.height(16.dp))
ReloadButton(
onClick = onReload
)
}
}

View File

@@ -22,7 +22,6 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
@@ -90,7 +89,7 @@ fun NotificationScreen() {
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.wrapContentHeight() .wrapContentHeight()
.padding(start = 16.dp, top = 8.dp), .padding(start = 16.dp, top = 8.dp, bottom = 16.dp),
horizontalArrangement = Arrangement.Start, horizontalArrangement = Arrangement.Start,
verticalAlignment = Alignment.Bottom verticalAlignment = Alignment.Bottom
) { ) {
@@ -107,7 +106,7 @@ fun NotificationScreen() {
TabSpacer() TabSpacer()
TabItem( TabItem(
text = stringResource(R.string.follow_upper), text = stringResource(R.string.followers_upper),
isSelected = pagerState.currentPage == 1, isSelected = pagerState.currentPage == 1,
onClick = { onClick = {
scope.launch { scope.launch {
@@ -136,9 +135,9 @@ fun NotificationScreen() {
.weight(1f) .weight(1f)
) { page -> ) { page ->
when (page) { when (page) {
0 -> LikeNoticeScreen(includeStatusBarPadding = false) 0 -> LikeNoticeScreen()
1 -> FollowerNoticeScreen(includeStatusBarPadding = false) 1 -> FollowerNoticeScreen()
2 -> CommentNoticeScreen(includeStatusBarPadding = false) 2 -> CommentNoticeScreen()
} }
} }
} }

View File

@@ -23,10 +23,12 @@ import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.systemBars
import androidx.compose.foundation.layout.asPaddingValues
import androidx.compose.material3.Button import androidx.compose.material3.Button
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.Surface
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.animation.core.animateDpAsState import androidx.compose.animation.core.animateDpAsState
@@ -54,6 +56,7 @@ import androidx.compose.ui.geometry.CornerRadius
import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.onSizeChanged import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.IntSize
@@ -103,37 +106,33 @@ fun PointsBottomSheet(
} }
} }
val statusBarPadding = WindowInsets.systemBars.asPaddingValues()
val configuration = LocalConfiguration.current
val screenHeight = configuration.screenHeightDp.dp
val offsetY = screenHeight * 0.07f - statusBarPadding.calculateTopPadding()
ModalBottomSheet( ModalBottomSheet(
onDismissRequest = onClose, // 允许通过代码关闭(如返回按钮) onDismissRequest = onClose, // 允许通过代码关闭(如返回按钮)
sheetState = sheetState, sheetState = sheetState,
containerColor = Color.Transparent, containerColor = AppColors.background,
dragHandle = null, // 移除拖动手柄 dragHandle = null // 移除拖动手柄
shape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp)
) { ) {
Box( Box(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(top = 10.dp) .fillMaxHeight(0.95f)
.offset(y = offsetY)
.padding(
start = 16.dp,
end = 16.dp,
bottom = 8.dp
)
) { ) {
Surface( Column(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.fillMaxHeight() .fillMaxHeight()
.clip(RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp)),
color = AppColors.background,
tonalElevation = 0.dp,
shadowElevation = 0.dp,
) { ) {
Column(
modifier = Modifier
.fillMaxWidth()
.fillMaxHeight()
.padding(
start = 16.dp,
end = 16.dp,
bottom = 8.dp
)
) {
// 头部 - 使用 Box 实现绝对居中布局 // 头部 - 使用 Box 实现绝对居中布局
Box( Box(
modifier = Modifier modifier = Modifier
@@ -341,7 +340,6 @@ fun PointsBottomSheet(
} else { } else {
HowToEarnList(onRecharge = onRecharge) HowToEarnList(onRecharge = onRecharge)
} }
}
} }
} }
} }

View File

@@ -13,30 +13,37 @@ import kotlinx.coroutines.launch
class CommentsViewModel( class CommentsViewModel(
var postId: String = 0.toString(), var postId: String = 0.toString(),
) : ViewModel() { ) : ViewModel() {
companion object {
private const val ORDER_ALL = "all"
private const val COMMENTS_PAGE_SIZE = 50
}
var commentService: CommentService = CommentServiceImpl() var commentService: CommentService = CommentServiceImpl()
var commentsList by mutableStateOf<List<CommentEntity>>(emptyList()) var commentsList by mutableStateOf<List<CommentEntity>>(emptyList())
var order: String by mutableStateOf(ORDER_ALL) var order: String by mutableStateOf("like")
var addedCommentList by mutableStateOf<List<CommentEntity>>(emptyList()) var addedCommentList by mutableStateOf<List<CommentEntity>>(emptyList())
var subCommentLoadingMap by mutableStateOf(mutableMapOf<Int, Boolean>()) var subCommentLoadingMap by mutableStateOf(mutableMapOf<Int, Boolean>())
var highlightCommentId by mutableStateOf<Int?>(null) var highlightCommentId by mutableStateOf<Int?>(null)
var highlightComment by mutableStateOf<CommentEntity?>(null) var highlightComment by mutableStateOf<CommentEntity?>(null)
var isLoading by mutableStateOf(false) var isLoading by mutableStateOf(false)
var hasError by mutableStateOf(false) var hasError by mutableStateOf(false)
var isLoadingMore by mutableStateOf(false)
var hasMore by mutableStateOf(false)
private var currentPage by mutableStateOf(0)
private var totalComments by mutableStateOf(0)
/** /**
* 预加载,在跳转到 PostScreen 之前设置好内容 * 预加载,在跳转到 PostScreen 之前设置好内容
*/ */
fun preTransit() { fun preTransit() {
reloadComment() viewModelScope.launch {
try {
isLoading = true
val response = commentService.getComments(
pageNumber = 1,
postId = postId.toInt(),
pageSize = 10
)
commentsList = response.list
hasError = false
} catch (e: Exception) {
e.printStackTrace()
hasError = true
} finally {
isLoading = false
}
}
} }
/** /**
@@ -44,61 +51,25 @@ class CommentsViewModel(
*/ */
fun reloadComment() { fun reloadComment() {
viewModelScope.launch { viewModelScope.launch {
loadComments(page = 1, reset = true) try {
}
}
fun loadMoreComments() {
if (isLoading || isLoadingMore || !hasMore) {
return
}
viewModelScope.launch {
loadComments(page = currentPage + 1, reset = false)
}
}
private suspend fun loadComments(page: Int, reset: Boolean) {
try {
if (reset) {
isLoading = true isLoading = true
val response = commentService.getComments(
pageNumber = 1,
postId = postId.toInt(),
order = order,
pageSize = 50
)
commentsList = response.list
hasError = false hasError = false
} else { } catch (e: Exception) {
isLoadingMore = true e.printStackTrace()
}
val response = commentService.getComments(
pageNumber = page,
postId = postId.toInt(),
order = normalizeOrder(order),
pageSize = COMMENTS_PAGE_SIZE
)
val total = response.total.coerceAtMost(Int.MAX_VALUE.toLong()).toInt()
totalComments = total
currentPage = response.page
commentsList = if (reset) {
response.list
} else {
commentsList + response.list
}
hasMore = commentsList.size < totalComments
} catch (e: Exception) {
e.printStackTrace()
if (reset) {
hasError = true hasError = true
commentsList = emptyList() } finally {
}
} finally {
if (reset) {
isLoading = false isLoading = false
} else {
isLoadingMore = false
} }
} }
} }
private fun normalizeOrder(currentOrder: String): String? {
return currentOrder.takeUnless { it.equals(ORDER_ALL, ignoreCase = true) }
}
suspend fun highlightComment(commentId: Int) { suspend fun highlightComment(commentId: Int) {
highlightCommentId = commentId highlightCommentId = commentId

View File

@@ -22,7 +22,6 @@ import androidx.compose.material3.Divider
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.Surface
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@@ -35,7 +34,6 @@ import android.graphics.BitmapFactory
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
@@ -80,108 +78,92 @@ fun DraftBoxBottomSheet(
ModalBottomSheet( ModalBottomSheet(
onDismissRequest = onDismiss, onDismissRequest = onDismiss,
sheetState = sheetState, sheetState = sheetState,
containerColor = Color.Transparent, containerColor = AppColors.background,
shape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp), shape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp),
dragHandle = {} dragHandle = {}
) { ) {
Box( Column(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(top = 8.dp) .fillMaxHeight(0.9f)
.padding(horizontal = 16.dp, vertical = 8.dp)
) { ) {
Surface( // 标题
Box(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.fillMaxHeight() .padding(vertical = 16.dp),
.clip(RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp)), contentAlignment = Alignment.Center
color = AppColors.background,
tonalElevation = 0.dp,
shadowElevation = 0.dp,
) { ) {
Column( Text(
text = stringResource(R.string.drafts),
fontSize = 17.sp,
fontWeight = FontWeight.Bold,
color = AppColors.text
)
}
// 草稿列表
if (drafts.isEmpty()) {
Box(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.fillMaxHeight() .fillMaxHeight(),
.padding(horizontal = 16.dp, vertical = 8.dp) contentAlignment = Alignment.Center
) { ) {
// 标题 Text(
Box( text = stringResource(R.string.no_drafts),
modifier = Modifier fontSize = 16.sp,
.fillMaxWidth() color = AppColors.secondaryText
.padding(vertical = 16.dp), )
contentAlignment = Alignment.Center }
) { } else {
Text( LazyColumn(
text = stringResource(R.string.drafts), modifier = Modifier
fontSize = 17.sp, .fillMaxWidth()
fontWeight = FontWeight.Bold, .weight(1f),
color = AppColors.text verticalArrangement = Arrangement.spacedBy(0.dp)
) ) {
} itemsIndexed(drafts) { index, draft ->
DraftItem(
// 草稿列表 draft = draft,
if (drafts.isEmpty()) { dateFormat = dateFormat,
Box( onEditClick = {
modifier = Modifier model.viewModelScope.launch {
.fillMaxWidth() model.loadDraft(context, draft)
.fillMaxHeight(), onDismiss()
contentAlignment = Alignment.Center
) {
Text(
text = stringResource(R.string.no_drafts),
fontSize = 16.sp,
color = AppColors.secondaryText
)
}
} else {
LazyColumn(
modifier = Modifier
.fillMaxWidth()
.weight(1f),
verticalArrangement = Arrangement.spacedBy(0.dp)
) {
itemsIndexed(drafts) { index, draft ->
DraftItem(
draft = draft,
dateFormat = dateFormat,
onEditClick = {
model.viewModelScope.launch {
model.loadDraft(context, draft)
onDismiss()
}
},
onDeleteClick = {
draftStore.deleteDraft(index)
drafts = draftStore.getAllDrafts()
},
AppColors = AppColors,
context = context
)
// 在草稿项之间添加分割线(最后一个不添加)
if (index < drafts.size - 1) {
Spacer(modifier = Modifier.height(12.dp))
Divider(
color = AppColors.secondaryText.copy(alpha = 0.2f),
thickness = 0.5.dp,
modifier = Modifier.padding(horizontal = 16.dp)
)
Spacer(modifier = Modifier.height(12.dp))
} }
} },
} onDeleteClick = {
draftStore.deleteDraft(index)
// 底部提示 drafts = draftStore.getAllDrafts()
Text( },
text = stringResource(R.string.only_save_the_last_5_drafts), AppColors = AppColors,
fontSize = 12.sp, context = context
color = AppColors.secondaryText,
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 8.dp),
textAlign = androidx.compose.ui.text.style.TextAlign.Center
) )
// 在草稿项之间添加分割线(最后一个不添加)
if (index < drafts.size - 1) {
Spacer(modifier = Modifier.height(12.dp))
Divider(
color = AppColors.secondaryText.copy(alpha = 0.2f),
thickness = 0.5.dp,
modifier = Modifier.padding(horizontal = 16.dp)
)
Spacer(modifier = Modifier.height(12.dp))
}
} }
} }
// 底部提示
Text(
text = stringResource(R.string.only_save_the_last_5_drafts),
fontSize = 12.sp,
color = AppColors.secondaryText,
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 8.dp),
textAlign = androidx.compose.ui.text.style.TextAlign.Center
)
} }
} }
} }

View File

@@ -1,12 +1,9 @@
package com.aiosman.ravenow.ui.post package com.aiosman.ravenow.ui.post
import android.Manifest
import android.content.pm.PackageManager
import android.net.Uri import android.net.Uri
import android.widget.Toast import android.widget.Toast
import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.content.ContextCompat
import androidx.compose.animation.core.LinearEasing import androidx.compose.animation.core.LinearEasing
import androidx.compose.animation.core.RepeatMode import androidx.compose.animation.core.RepeatMode
import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.core.animateFloatAsState
@@ -591,34 +588,6 @@ fun AddImageGrid() {
} }
} }
// 摄像头权限请求
val requestCameraPermissionLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.RequestPermission()
) { isGranted ->
if (isGranted) {
// 权限已授予,打开相机
if (model.imageList.size < 9) {
val photoFile = File(context.cacheDir, "photo.jpg")
val photoUri: Uri = FileProvider.getUriForFile(
context,
"${context.packageName}.fileprovider",
photoFile
)
model.currentPhotoUri = photoUri
takePictureLauncher.launch(photoUri)
} else {
Toast.makeText(context, "最多只能选择9张图片", Toast.LENGTH_SHORT).show()
}
} else {
// 权限被拒绝,提示用户
Toast.makeText(
context,
"需要摄像头权限才能拍摄照片,请在设置中开启",
Toast.LENGTH_LONG
).show()
}
}
val addImageDebouncer = rememberDebouncer() val addImageDebouncer = rememberDebouncer()
val canAddMoreImages = model.imageList.size < 9 val canAddMoreImages = model.imageList.size < 9
@@ -673,27 +642,14 @@ fun AddImageGrid() {
.background(Color(0xFFFAF9FB)) .background(Color(0xFFFAF9FB))
.noRippleClickable { .noRippleClickable {
if (model.imageList.size < 9) { if (model.imageList.size < 9) {
// 检查摄像头权限 val photoFile = File(context.cacheDir, "photo.jpg")
when { val photoUri: Uri = FileProvider.getUriForFile(
ContextCompat.checkSelfPermission( context,
context, "${context.packageName}.fileprovider",
Manifest.permission.CAMERA photoFile
) == PackageManager.PERMISSION_GRANTED -> { )
// 已有权限,直接打开相机 model.currentPhotoUri = photoUri
val photoFile = File(context.cacheDir, "photo.jpg") takePictureLauncher.launch(photoUri)
val photoUri: Uri = FileProvider.getUriForFile(
context,
"${context.packageName}.fileprovider",
photoFile
)
model.currentPhotoUri = photoUri
takePictureLauncher.launch(photoUri)
}
else -> {
// 没有权限,请求权限
requestCameraPermissionLauncher.launch(Manifest.permission.CAMERA)
}
}
} else { } else {
Toast.makeText(context, "最多只能选择9张图片", Toast.LENGTH_SHORT).show() Toast.makeText(context, "最多只能选择9张图片", Toast.LENGTH_SHORT).show()
} }

View File

@@ -12,8 +12,6 @@ import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutVertically import androidx.compose.animation.slideOutVertically
import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.Image import androidx.compose.foundation.Image
import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.gestures.detectTapGestures import androidx.compose.foundation.gestures.detectTapGestures
@@ -26,7 +24,6 @@ import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.asPaddingValues import androidx.compose.foundation.layout.asPaddingValues
import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize 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
@@ -47,7 +44,6 @@ import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@@ -304,35 +300,20 @@ fun PostScreen(
onDismissRequest = { onDismissRequest = {
showReportDialog = false showReportDialog = false
}, },
containerColor = Color.Transparent, containerColor = AppColors.background,
sheetState = rememberModalBottomSheetState( sheetState = rememberModalBottomSheetState(
skipPartiallyExpanded = true skipPartiallyExpanded = true
), ),
dragHandle = {}, dragHandle = {},
shape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp), shape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp),
) { ) {
Box( ReportModal(
modifier = Modifier momentId = viewModel.moment!!.id,
.fillMaxWidth() onClose = {
.padding(top = 7.dp) showReportDialog = false
) {
Surface(
modifier = Modifier
.fillMaxWidth()
.clip(RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp)),
tonalElevation = 0.dp,
shadowElevation = 0.dp,
color = AppColors.background,
contentColor = AppColors.text
) {
ReportModal(
momentId = viewModel.moment!!.id,
onClose = {
showReportDialog = false
}
)
} }
} )
} }
} }
Scaffold( Scaffold(
@@ -512,9 +493,7 @@ fun PostScreen(
color = AppColors.nonActiveText color = AppColors.nonActiveText
) )
Spacer(modifier = Modifier.weight(1f)) Spacer(modifier = Modifier.weight(1f))
OrderSelectionComponent( OrderSelectionComponent() {
selectedOrder = commentsViewModel.order
) {
commentsViewModel.order = it commentsViewModel.order = it
viewModel.reloadComment() viewModel.reloadComment()
} }
@@ -761,33 +740,6 @@ fun CommentContent(
} }
} }
if (viewModel.isLoadingMore || viewModel.hasMore) {
Box(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 12.dp),
contentAlignment = Alignment.Center
) {
if (viewModel.isLoadingMore) {
CircularProgressIndicator(
modifier = Modifier.size(24.dp),
color = AppColors.main,
strokeWidth = 2.dp
)
} else {
Text(
text = stringResource(id = R.string.load_more),
color = AppColors.main,
fontSize = 14.sp,
fontWeight = FontWeight.SemiBold,
modifier = Modifier.noRippleClickable {
viewModel.loadMoreComments()
}
)
}
}
}
// 加载状态处理 // 加载状态处理
if (viewModel.isLoading) { if (viewModel.isLoading) {
Box( Box(
@@ -1207,26 +1159,14 @@ fun ImageViewerDialog(
} }
} }
@OptIn(ExperimentalFoundationApi::class)
@Composable @Composable
fun PostImageView( fun PostImageView(
images: List<MomentImageEntity>, images: List<MomentImageEntity>,
initialPage: Int? = 0 initialPage: Int? = 0
) { ) {
val context = LocalContext.current val context = LocalContext.current
val scope = rememberCoroutineScope()
var isImageViewerDialog by remember { mutableStateOf(false) } var isImageViewerDialog by remember { mutableStateOf(false) }
val initialPageIndex = initialPage ?: 0 var currentImageIndex by remember { mutableStateOf(initialPage ?: 0) }
val pagerState = rememberPagerState(
pageCount = { images.size },
initialPage = initialPageIndex.coerceIn(0, maxOf(0, images.size - 1))
)
var currentImageIndex by remember { mutableStateOf(pagerState.currentPage) }
// 同步 pagerState 的当前页面到 currentImageIndex
LaunchedEffect(pagerState.currentPage) {
currentImageIndex = pagerState.currentPage
}
DisposableEffect(Unit) { DisposableEffect(Unit) {
onDispose { onDispose {
@@ -1247,31 +1187,23 @@ fun PostImageView(
modifier = Modifier modifier = Modifier
) { ) {
if (images.isNotEmpty()) { if (images.isNotEmpty()) {
HorizontalPager( CustomAsyncImage(
state = pagerState, context,
images[currentImageIndex].thumbnail,
contentDescription = "Image",
contentScale = ContentScale.Crop,
modifier = Modifier modifier = Modifier
.weight(1f) .weight(1f)
.fillMaxWidth() .fillMaxWidth()
) { page -> .pointerInput(Unit) {
val image = images[page] detectTapGestures(
CustomAsyncImage( onTap = {
context, isImageViewerDialog = true
image.thumbnail, }
contentDescription = "Image", )
blurHash = image.blurHash, }
contentScale = ContentScale.Crop, .background(Color.Gray.copy(alpha = 0.1f))
modifier = Modifier )
.fillMaxSize()
.pointerInput(Unit) {
detectTapGestures(
onTap = {
isImageViewerDialog = true
}
)
}
.background(Color.Gray.copy(alpha = 0.1f))
)
}
} }
// 图片导航控件 // 图片导航控件
@@ -1280,26 +1212,56 @@ fun PostImageView(
modifier = Modifier modifier = Modifier
.padding(8.dp) .padding(8.dp)
.fillMaxWidth(), .fillMaxWidth(),
horizontalArrangement = Arrangement.Center, horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
// Previous button
Text(
text = "Previous",
modifier = Modifier
.padding(8.dp)
.noRippleClickable {
if (currentImageIndex > 0) {
currentImageIndex--
}
},
color = if (currentImageIndex > 0) Color.Blue else Color.Gray
)
// Indicators // Indicators
images.forEachIndexed { index, _ -> Row(
Box( horizontalArrangement = Arrangement.Center
modifier = Modifier ) {
.size(4.dp) images.forEachIndexed { index, _ ->
.clip(CircleShape) Box(
.background( modifier = Modifier
if (pagerState.currentPage == index) Color.Red else Color.Gray.copy( .size(4.dp)
alpha = 0.5f .clip(CircleShape)
.background(
if (currentImageIndex == index) Color.Red else Color.Gray.copy(
alpha = 0.5f
)
) )
) .padding(4.dp)
.padding(4.dp) )
) if (index < images.size - 1) {
if (index < images.size - 1) { Spacer(modifier = Modifier.width(8.dp))
Spacer(modifier = Modifier.width(8.dp)) }
} }
} }
// Next button
Text(
text = "Next",
modifier = Modifier
.padding(8.dp)
.noRippleClickable {
if (currentImageIndex < images.size - 1) {
currentImageIndex++
}
},
color = if (currentImageIndex < images.size - 1) Color.Blue else Color.Gray
)
} }
} }
} }
@@ -1955,15 +1917,15 @@ fun CommentMenuModal(
@Composable @Composable
fun OrderSelectionComponent( fun OrderSelectionComponent(
selectedOrder: String,
onSelected: (String) -> Unit = {} onSelected: (String) -> Unit = {}
) { ) {
val AppColors = LocalAppTheme.current val AppColors = LocalAppTheme.current
var selectedOrder by remember { mutableStateOf("like") }
val orders = listOf( val orders = listOf(
"all" to stringResource(R.string.order_comment_default), "like" to stringResource(R.string.order_comment_default),
"latest" to stringResource(R.string.order_comment_latest), "earliest" to stringResource(R.string.order_comment_earliest),
"like" to stringResource(R.string.order_comment_hot) "latest" to stringResource(R.string.order_comment_latest)
) )
Box( Box(
modifier = Modifier modifier = Modifier
@@ -1981,9 +1943,8 @@ fun OrderSelectionComponent(
Box( Box(
modifier = Modifier modifier = Modifier
.noRippleClickable { .noRippleClickable {
if (selectedOrder != order.first) { selectedOrder = order.first
onSelected(order.first) onSelected(order.first)
}
} }
.background( .background(
if ( if (
@@ -2050,8 +2011,8 @@ fun ReportModal(
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.fillMaxHeight() .background(AppColors.background)
.padding(start = 24.dp, end = 24.dp) .padding(start = 24.dp, end = 24.dp, bottom = 64.dp)
) { ) {
Box( Box(
modifier = Modifier.fillMaxWidth().padding(top = 16.dp, bottom = 16.dp), modifier = Modifier.fillMaxWidth().padding(top = 16.dp, bottom = 16.dp),

View File

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

View File

@@ -1,6 +1,7 @@
package com.aiosman.ravenow.utils package com.aiosman.ravenow.utils
import com.aiosman.ravenow.AppState import com.aiosman.ravenow.AppState
import com.aiosman.ravenow.entity.OpenIMMessageType
import com.google.gson.Gson import com.google.gson.Gson
import com.google.gson.JsonSyntaxException import com.google.gson.JsonSyntaxException
import io.openim.android.sdk.models.Message import io.openim.android.sdk.models.Message
@@ -25,8 +26,21 @@ object MessageParser {
val gson = Gson() val gson = Gson()
val message = gson.fromJson(latestMsgJson, Message::class.java) val message = gson.fromJson(latestMsgJson, Message::class.java)
// 判断是否是自己发送的消息 // 检查是否为通知类型消息
isSelf = message.sendID == AppState.profile?.trtcUserId // 1. 检查消息类型是否为通知类型
// 2. 检查发送者ID是否为系统账户如 "imAdmin"、"administrator" 等)
val sendID = message.sendID ?: ""
val isSystemAccount = sendID == "imAdmin" || sendID == "administrator" || sendID.isEmpty()
val isNotificationType = OpenIMMessageType.isNotification(message.contentType)
val isNotification = isNotificationType || isSystemAccount
// 通知类型消息不显示"我:"前缀,设置 isSelf = false
if (isNotification) {
isSelf = false
} else {
// 判断是否是自己发送的消息
isSelf = sendID == AppState.profile?.trtcUserId
}
// 根据消息类型生成显示文本 // 根据消息类型生成显示文本
displayText = getMessageDisplayText(message) displayText = getMessageDisplayText(message)
@@ -50,6 +64,19 @@ object MessageParser {
* @return 消息的显示文本 * @return 消息的显示文本
*/ */
private fun getMessageDisplayText(message: Message): String { private fun getMessageDisplayText(message: Message): String {
// 检查是否为通知类型消息
// 1. 检查消息类型是否为通知类型
// 2. 检查发送者ID是否为系统账户如 "imAdmin"、"administrator" 等)
val sendID = message.sendID ?: ""
val isSystemAccount = sendID == "imAdmin" || sendID == "administrator" || sendID.isEmpty()
val isNotificationType = OpenIMMessageType.isNotification(message.contentType)
val isNotification = isNotificationType || isSystemAccount
if (isNotification) {
// 使用 NotificationMessageHelper 生成通知文本
return NotificationMessageHelper.getNotificationText(message)
}
return when (message.contentType) { return when (message.contentType) {
101 -> { // TEXT 101 -> { // TEXT
message.textElem?.content ?: "[文本消息]" message.textElem?.content ?: "[文本消息]"

View File

@@ -0,0 +1,73 @@
package com.aiosman.ravenow.utils
import com.aiosman.ravenow.entity.OpenIMMessageType
import io.openim.android.sdk.models.Message
/**
* 通知消息辅助工具类
* 用于生成通知消息的显示文本
*/
object NotificationMessageHelper {
/**
* 从通知消息中提取显示文本
* @param message OpenIM 消息对象
* @return 通知消息的显示文本
*/
fun getNotificationText(message: Message): String {
// 优先尝试从 textElem 中获取内容(系统通知通常使用文本消息格式)
val textContent = message.textElem?.content
if (textContent != null && textContent.isNotEmpty()) {
return textContent
}
// 如果发送者是系统账户但没有文本内容,根据消息类型生成友好的中文提示
// 根据消息类型生成友好的中文提示
return when (message.contentType) {
OpenIMMessageType.FRIEND_ADDED -> "你们已成为好友,可以开始聊天了"
OpenIMMessageType.SYSTEM_NOTIFICATION -> "系统通知"
OpenIMMessageType.GROUP_CREATED -> "群聊已创建"
OpenIMMessageType.GROUP_INFO_CHANGED -> "群信息已更新"
OpenIMMessageType.GROUP_MEMBER_QUIT -> {
val memberName = message.senderNickname ?: "成员"
"$memberName 退出了群聊"
}
OpenIMMessageType.GROUP_OWNER_CHANGED -> {
val newOwnerName = message.senderNickname ?: "成员"
"群主已更换为 $newOwnerName"
}
OpenIMMessageType.GROUP_MEMBER_KICKED -> {
val memberName = message.senderNickname ?: "成员"
"$memberName 被移出群聊"
}
OpenIMMessageType.GROUP_MEMBER_INVITED -> {
val memberName = message.senderNickname ?: "成员"
"$memberName 加入了群聊"
}
OpenIMMessageType.GROUP_MEMBER_JOINED -> {
val memberName = message.senderNickname ?: "成员"
"$memberName 加入了群聊"
}
OpenIMMessageType.GROUP_DISMISSED -> "群聊已解散"
OpenIMMessageType.GROUP_MEMBER_MUTED -> {
val memberName = message.senderNickname ?: "成员"
"$memberName 已被禁言"
}
OpenIMMessageType.GROUP_MEMBER_UNMUTED -> {
val memberName = message.senderNickname ?: "成员"
"$memberName 已解除禁言"
}
OpenIMMessageType.GROUP_MUTED -> "群聊已开启全员禁言"
OpenIMMessageType.GROUP_UNMUTED -> "群聊已关闭全员禁言"
OpenIMMessageType.GROUP_ANNOUNCEMENT_CHANGED -> "群公告已更新"
OpenIMMessageType.GROUP_NAME_CHANGED -> "群名称已更改"
OpenIMMessageType.SNAPCHAT_TOGGLE -> "阅后即焚功能已更改"
OpenIMMessageType.MESSAGE_REVOKED -> {
val senderName = message.senderNickname ?: "成员"
"$senderName 撤回了一条消息"
}
else -> OpenIMMessageType.getDescription(message.contentType)
}
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1006 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 866 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 798 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 935 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 886 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 829 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 724 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 656 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 618 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 916 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 715 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 882 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 800 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 860 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 864 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 675 B

Some files were not shown because too many files have changed in this diff Show More