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"
minSdk = 24
targetSdk = 35
versionCode = 1000019
versionName = "1.0.000.19"
versionCode = 1000021
versionName = "1.0.000.21"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables {
@@ -140,5 +140,8 @@ dependencies {
implementation(libs.androidx.room.ktx)
ksp(libs.androidx.room.compiler)
// Google Play Billing
implementation(libs.billing.ktx)
}

View File

@@ -8,6 +8,7 @@
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
<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" />
<application
@@ -54,7 +55,7 @@
android:theme="@style/Theme.App.Starting"
android:screenOrientation="portrait"
android:windowSoftInputMode="adjustResize"
android:configChanges="fontScale|orientation|screenSize|keyboardHidden|uiMode">
android:configChanges="fontScale|orientation|screenSize|keyboardHidden">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />

View File

@@ -73,10 +73,6 @@ class MainActivity : ComponentActivity() {
val config = Configuration(newConfig)
config.fontScale = 1.0f
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)
updateWindowBackground(AppState.darkMode)
if (AppState.darkMode) {
window.decorView.setBackgroundColor(android.graphics.Color.BLACK)
}
enableEdgeToEdge()
scope.launch {
@@ -271,22 +269,8 @@ class MainActivity : ComponentActivity() {
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> {
error("NavController not provided")
}

View File

@@ -4,6 +4,10 @@ import android.app.Application
import android.content.Context
import android.content.res.Configuration
import android.util.Log
import com.android.billingclient.api.BillingClient
import com.android.billingclient.api.BillingClientStateListener
import com.android.billingclient.api.BillingResult
import com.android.billingclient.api.PurchasesUpdatedListener
import com.google.firebase.FirebaseApp
import com.google.firebase.perf.FirebasePerformance
@@ -12,6 +16,8 @@ import com.google.firebase.perf.FirebasePerformance
*/
class RaveNowApplication : Application() {
private var billingClient: BillingClient? = null
override fun attachBaseContext(base: Context) {
// 禁用字体缩放,固定字体大小为系统默认大小
val configuration = Configuration(base.resources.configuration)
@@ -48,6 +54,53 @@ class RaveNowApplication : Application() {
} catch (e: Exception) {
Log.e("RaveNowApplication", "Firebase初始化失败在进程 $processName", e)
}
// 初始化 Google Play Billing
initBillingClient()
}
/**
* 初始化 Google Play Billing Client
*/
private fun initBillingClient() {
val purchasesUpdatedListener = PurchasesUpdatedListener { billingResult, purchases ->
if (billingResult.responseCode == BillingClient.BillingResponseCode.OK && purchases != null) {
// 处理购买成功
Log.d("RaveNowApplication", "购买成功: ${purchases.size} 个商品")
} else if (billingResult.responseCode == BillingClient.BillingResponseCode.USER_CANCELED) {
// 用户取消购买
Log.d("RaveNowApplication", "用户取消购买")
} else {
// 处理其他错误
Log.e("RaveNowApplication", "购买失败: ${billingResult.debugMessage}")
}
}
billingClient = BillingClient.newBuilder(this)
.setListener(purchasesUpdatedListener)
.build()
billingClient?.startConnection(object : BillingClientStateListener {
override fun onBillingSetupFinished(billingResult: BillingResult) {
if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
Log.d("RaveNowApplication", "BillingClient 初始化成功")
} else {
Log.e("RaveNowApplication", "BillingClient 初始化失败: ${billingResult.debugMessage}")
}
}
override fun onBillingServiceDisconnected() {
Log.w("RaveNowApplication", "BillingClient 连接断开,尝试重新连接")
// 可以在这里实现重连逻辑
}
})
}
/**
* 获取 BillingClient 实例
*/
fun getBillingClient(): BillingClient? {
return billingClient
}
/**

View File

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

View File

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

View File

@@ -4,6 +4,7 @@ import android.content.Context
import android.icu.util.Calendar
import com.aiosman.ravenow.ConstVars
import com.aiosman.ravenow.exp.formatChatTime
import com.aiosman.ravenow.utils.NotificationMessageHelper
import com.google.gson.annotations.SerializedName
import io.openim.android.sdk.models.Message
import io.openim.android.sdk.models.PictureElem
@@ -21,7 +22,8 @@ data class ChatItem(
val textDisplay: String = "",
val msgId: String, // Add this property
var showTimestamp: Boolean = false,
var showTimeDivider: Boolean = false
var showTimeDivider: Boolean = false,
val isNotification: Boolean = false // 标识是否为通知类型消息
) {
companion object {
// OpenIM 消息类型常量
@@ -36,6 +38,32 @@ data class ChatItem(
val calendar = Calendar.getInstance()
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
if (faceAvatar == null) {
faceAvatar = "${ConstVars.BASE_SERVER}${message.senderFaceUrl}"
@@ -62,7 +90,8 @@ data class ChatItem(
).toMutableList(),
messageType = MESSAGE_TYPE_IMAGE,
textDisplay = "Image",
msgId = message.clientMsgID
msgId = message.clientMsgID,
isNotification = false
)
}
return null
@@ -79,7 +108,8 @@ data class ChatItem(
imageList = emptyList<PictureInfo>().toMutableList(),
messageType = MESSAGE_TYPE_TEXT,
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) {
this.list = this.list.map { momentItem ->
if (momentItem.id == id) {
// 只有当状态发生变化时才更新计数,避免重复更新
val countDelta = if (momentItem.liked != isLike) {
if (isLike) 1 else -1
} else {
0
}
momentItem.copy(
likeCount = (momentItem.likeCount + countDelta).coerceAtLeast(0),
liked = isLike
)
momentItem.copy(likeCount = momentItem.likeCount + if (isLike) 1 else -1, liked = isLike)
} else {
momentItem
}
@@ -430,16 +421,7 @@ class MomentLoader : DataLoader<MomentEntity,MomentLoaderExtraArgs>() {
fun updateFavoriteCount(id: Int,isFavorite:Boolean) {
this.list = this.list.map { momentItem ->
if (momentItem.id == id) {
// 只有当状态发生变化时才更新计数,避免重复更新
val countDelta = if (momentItem.isFavorite != isFavorite) {
if (isFavorite) 1 else -1
} else {
0
}
momentItem.copy(
favoriteCount = (momentItem.favoriteCount + countDelta).coerceAtLeast(0),
isFavorite = isFavorite
)
momentItem.copy(favoriteCount = momentItem.favoriteCount + if (isFavorite) 1 else -1, isFavorite = isFavorite)
} else {
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
import android.util.Log
import androidx.paging.PagingSource
import androidx.paging.PagingState
import com.aiosman.ravenow.data.ListContainer
@@ -273,41 +272,19 @@ class RoomRemoteDataSource {
pageSize: Int = 20,
search: String
): ListContainer<RoomEntity>? {
return try {
val resp = ApiClient.api.getRooms(
page = pageNumber,
pageSize = pageSize,
search = search,
roomType = "public" // 搜索时只显示公有房间
)
if (!resp.isSuccessful) {
// API 调用失败,返回 null
return null
}
val body = resp.body() ?: return null
// 安全地转换数据,过滤掉转换失败的项目
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
}
val resp = ApiClient.api.getRooms(
page = pageNumber,
pageSize = pageSize,
search = search,
roomType = "public" // 搜索时只显示公有房间
)
val body = resp.body() ?: return null
return ListContainer(
total = body.total,
page = pageNumber,
pageSize = pageSize,
list = body.list.map { it.toRoomtEntity() }
)
}
}
@@ -326,31 +303,17 @@ class RoomSearchPagingSource(
pageSize = params.loadSize,
search = keyword
)
if (rooms == null) {
// API 调用失败,返回空列表
LoadResult.Page(
data = emptyList(),
prevKey = if (currentPage == 1) null else currentPage - 1,
nextKey = null
)
} 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.Page(
data = rooms?.list ?: listOf(),
prevKey = if (currentPage == 1) null else currentPage - 1,
nextKey = if (rooms?.list?.isNotEmpty() == true) currentPage + 1 else null
)
} catch (exception: IOException) {
LoadResult.Error(exception)
}
}
override fun getRefreshKey(state: PagingState<Int, RoomEntity>): Int? {
// 更健壮的实现:根据 anchorPosition 计算刷新键
return state.anchorPosition?.let { anchorPosition ->
state.closestPageToPosition(anchorPosition)?.prevKey?.plus(1)
?: state.closestPageToPosition(anchorPosition)?.nextKey?.minus(1)
}
return state.anchorPosition
}
}

View File

@@ -2,7 +2,6 @@ package com.aiosman.ravenow
import android.content.Context
import android.content.SharedPreferences
import android.content.res.Configuration
import com.google.android.gms.auth.api.signin.GoogleSignInOptions
/**
@@ -25,16 +24,11 @@ object AppStore {
.requestEmail()
.build()
googleSignInOptions = gso
// apply dark mode - 如果用户未手动设置,优先跟随系统
val hasUserPreference = sharedPreferences.contains("darkMode")
val resolvedDarkMode = if (hasUserPreference) {
sharedPreferences.getBoolean("darkMode", false)
} else {
val currentNightMode = context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK
currentNightMode == Configuration.UI_MODE_NIGHT_YES
// apply dark mode
if (sharedPreferences.getBoolean("darkMode", false)) {
AppState.darkMode = true
AppState.appTheme = DarkThemeColors()
}
AppState.darkMode = resolvedDarkMode
AppState.appTheme = if (resolvedDarkMode) DarkThemeColors() else LightThemeColors()
// load chat background
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.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
@@ -41,21 +40,22 @@ fun AboutScreen() {
modifier = Modifier.padding(horizontal = 24.dp, vertical = 16.dp)
) {
NoticeScreenHeader(
title = stringResource(R.string.about_paipai),
title = stringResource(R.string.about_rave_now),
moreIcon = false
)
}
Column(
modifier = Modifier
.weight(1f)
.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
.fillMaxWidth()
.padding(start = 24.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
// app icondww
Spacer(modifier = Modifier.height(48.dp))
// app icon
Box {
Image(
painter = painterResource(id = R.mipmap.invalid_name),
painter = painterResource(id = R.mipmap.rider_pro_color_logo_next),
contentDescription = "app icon",
modifier = Modifier.size(80.dp)
)
@@ -63,7 +63,7 @@ fun AboutScreen() {
Spacer(modifier = Modifier.height(24.dp))
// app name
Text(
text = stringResource(R.string.paipai),
text = "Rave Now".uppercase(),
fontSize = 24.sp,
color = appColors.text,
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
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.asPaddingValues
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.systemBars
import androidx.compose.foundation.layout.width
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.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.Surface
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
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.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.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 androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Check
import com.aiosman.ravenow.AppState
import com.aiosman.ravenow.LocalAppTheme
import com.aiosman.ravenow.LocalNavController
import com.aiosman.ravenow.R
import com.aiosman.ravenow.ui.modifiers.noRippleClickable
import com.aiosman.ravenow.ui.comment.NoticeScreenHeader
// MBTI类型列表
val MBTI_TYPES = listOf(
@@ -66,318 +46,96 @@ val MBTI_TYPES = listOf(
"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
fun MbtiSelectBottomSheet(
onClose: () -> Unit
) {
fun MbtiSelectScreen() {
val navController = LocalNavController.current
val appColors = LocalAppTheme.current
val isDarkMode = AppState.darkMode
val model = AccountEditViewModel
val currentMbti = model.mbti
val sheetBackgroundColor = if (isDarkMode) {
appColors.secondaryBackground
} else {
Color(0xFFFFFFFF)
}
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
ModalBottomSheet(
onDismissRequest = onClose,
sheetState = sheetState,
containerColor = Color.Transparent,
shape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp),
dragHandle = {}
Column(
modifier = Modifier
.fillMaxSize()
.background(appColors.profileBackground)
) {
// 头部
Box(
modifier = Modifier
.fillMaxWidth()
.fillMaxHeight()
.padding(top = 8.dp)
.padding(vertical = 16.dp)
) {
Surface(
modifier = Modifier
.fillMaxWidth()
.fillMaxHeight()
.clip(RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp)),
color = sheetBackgroundColor,
tonalElevation = 0.dp,
shadowElevation = 0.dp,
) {
Column(
modifier = Modifier
.fillMaxWidth()
.fillMaxHeight()
.padding(horizontal = 16.dp, vertical = 8.dp)
) {
// 头部
Box(
modifier = Modifier
.fillMaxWidth()
.height(48.dp),
contentAlignment = Alignment.Center
) {
val cancelButtonGradientColors = if (isDarkMode) {
listOf(
Color(0xFF3A3A3C),
Color(0xFF2C2C2E)
)
} else {
listOf(
Color(0xFFFFFFFF),
Color(0xFFF8F8F8)
)
NoticeScreenHeader(
title = stringResource(R.string.choose_mbti),
moreIcon = false
)
}
// 列表
LazyColumn(
modifier = Modifier.fillMaxSize(),
contentPadding = androidx.compose.foundation.layout.PaddingValues(horizontal = 16.dp, vertical = 8.dp)
) {
items(MBTI_TYPES) { mbti ->
MBTIItem(
mbti = mbti,
isSelected = mbti == currentMbti,
onClick = {
model.mbti = mbti
// 立即保存到本地存储,确保选择后立即生效
AppState.UserId?.let { uid ->
com.aiosman.ravenow.AppStore.setUserMbti(uid, mbti)
}
val cancelButtonContentColor = if (isDarkMode) Color(0xFFFFFFFF) else Color(0xFF404040)
// 左上角返回按钮:整体「箭头 + 取消」在按钮内居中
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
)
navController.navigateUp()
}
Spacer(Modifier.height(12.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))
}
}
}
}
}
)
Spacer(modifier = Modifier.height(8.dp))
}
}
}
}
// 保留原有的 MbtiSelectScreen 用于导航路由(如果需要)
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun MbtiSelectScreen() {
val navController = LocalNavController.current
MbtiSelectBottomSheet(
onClose = {
navController.navigateUp()
}
)
}
@Composable
fun MbtiItem(
fun MBTIItem(
mbti: String,
isSelected: Boolean,
onClick: () -> Unit
) {
val appColors = LocalAppTheme.current
val isDarkMode = AppState.darkMode
// 卡片背景色
val cardBackgroundColor = if (isDarkMode) {
Color(0xFF2A2A2A) // 比 secondaryBackground (0xFF1C1C1C) 更亮的灰色
} else {
Color(0xFFFAF9FB)
}
Column(
Box(
modifier = Modifier
.fillMaxWidth()
.aspectRatio(1.1f)
.shadow(
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)
.clip(RoundedCornerShape(16.dp))
.background(if (isSelected) appColors.main.copy(alpha = 0.1f) else Color.White)
.clickable(
indication = null,
interactionSource = remember { MutableInteractionSource() }
) {
onClick()
}
.padding(horizontal = 16.dp, vertical = 12.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
.padding(16.dp)
) {
// 直接把 MBTI 图标和文字放在灰色卡片内部,布局与星座保持一致
Image(
painter = painterResource(id = getMbtiImageResId(mbti, isDarkMode)),
contentDescription = mbti,
modifier = Modifier.size(96.dp)
)
Spacer(modifier = Modifier.height(0.dp))
Text(
text = mbti,
fontSize = 14.sp,
fontWeight = FontWeight.Medium,
color = appColors.text,
textAlign = TextAlign.Center,
modifier = Modifier.offset(y = (-10).dp)
)
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = mbti,
fontSize = 17.sp,
fontWeight = FontWeight.Normal,
color = if (isSelected) appColors.main else appColors.text,
modifier = Modifier.weight(1f)
)
if (isSelected) {
Icon(
imageVector = Icons.Default.Check,
contentDescription = "Selected",
modifier = Modifier.size(20.dp),
tint = appColors.main
)
}
}
}
}

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.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.rememberModalBottomSheetState
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.text.font.FontWeight
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.NestedScrollSource
import androidx.compose.ui.input.nestedscroll.nestedScroll
@@ -136,165 +134,172 @@ fun ZodiacSelectBottomSheet(
}
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(
onDismissRequest = onClose,
sheetState = sheetState,
// 对齐发布动态草稿箱样式:底层透明,内容区域自己绘制圆角和背景
containerColor = Color.Transparent,
shape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp),
dragHandle = {}
containerColor = sheetBackgroundColor, // 根据主题自适应背景
dragHandle = null
) {
Box(
modifier = Modifier
.fillMaxWidth()
.fillMaxHeight()
.padding(top = 8.dp)
.fillMaxHeight(0.95f)
.offset(y = offsetY)
.padding(
start = 16.dp,
end = 16.dp,
bottom = 8.dp
)
) {
Surface(
Column(
modifier = Modifier
.fillMaxWidth()
.fillMaxHeight()
.clip(RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp)),
color = sheetBackgroundColor,
tonalElevation = 0.dp,
shadowElevation = 0.dp,
) {
Column(
// 头部 - 使用 Box 实现绝对居中布局
Box(
modifier = Modifier
.fillMaxWidth()
.fillMaxHeight()
.padding(horizontal = 16.dp, vertical = 8.dp)
.height(48.dp),
contentAlignment = Alignment.Center
) {
// 头部 - 使用 Box 实现绝对居中布局(对齐草稿箱样式)
Box(
modifier = Modifier
.fillMaxWidth()
.height(48.dp),
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)
val cancelButtonGradientColors = if (isDarkMode) {
listOf(
Color(0xFF3A3A3C),
Color(0xFF2C2C2E)
)
} else {
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
)
// 左上角返回按钮 - 根据 Swift 代码样式,带淡灰色渐变背景
Row(
modifier = Modifier
.align(Alignment.CenterStart)
.height(36.dp)
.clip(RoundedCornerShape(18.dp)) // 圆角 100.0 在 36dp 高度下接近完全圆角
.background(
brush = Brush.linearGradient(
colors = cancelButtonGradientColors
// 不指定 start 和 end默认从左上到右下
)
.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
)
}
}
)
.noRippleClickable { onClose() }
.padding(horizontal = 8.dp), // 内部 padding 确保内容不贴边
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)
)
// "取消" 文字
Text(
text = stringResource(R.string.choose_zodiac),
color = appColors.text,
fontSize = 20.sp,
fontWeight = FontWeight.Bold,
text = "取消",
fontSize = 17.sp,
fontWeight = FontWeight.Medium,
color = cancelButtonContentColor,
textAlign = androidx.compose.ui.text.style.TextAlign.Center
)
}
// 中间标题 - 绝对居中
Text(
text = stringResource(R.string.choose_zodiac),
color = appColors.text,
fontSize = 20.sp,
fontWeight = FontWeight.Bold,
textAlign = androidx.compose.ui.text.style.TextAlign.Center
)
}
Spacer(Modifier.height(12.dp))
Spacer(Modifier.height(12.dp))
// 创建 NestedScrollConnection
// 1. 不抢在列表前面消费事件,让 LazyVerticalGrid 正常滚动
// 2. 在列表滚动之后把剩余滚动吃掉,避免继续传递到 BottomSheet 去触发下拉关闭
val nestedScrollConnection = remember {
object : NestedScrollConnection {
override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
// 不在这里消费,先让 LazyVerticalGrid 自己处理滚动
return Offset.Zero
}
// 创建 NestedScrollConnection 来阻止滚动事件向上传播到 ModalBottomSheet
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 {
// 列表滚动完之后,把剩余滚动(尤其是向下拖拽)全部吃掉,防止再传给 BottomSheet
return available
}
override fun onPostScroll(consumed: Offset, available: Offset, source: NestedScrollSource): Offset {
// 消费 LazyVerticalGrid 处理后的剩余滚动事件,防止传递到 ModalBottomSheet
return available
}
override suspend fun onPreFling(available: Velocity): Velocity {
// 不抢在列表前面处理 fling,让 LazyVerticalGrid 先做惯性滚动
return Velocity.Zero
}
override suspend fun onPreFling(available: Velocity): Velocity {
// 不消费惯性滚动,让 LazyVerticalGrid 先处理
return Velocity.Zero
}
override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity {
// 列表惯性滚动之后,把剩余的 fling 速度吃掉,避免带动 BottomSheet 下滑关闭
return available
}
override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity {
// 消费 LazyVerticalGrid 处理后的剩余惯性滚动,防止传递到 ModalBottomSheet
return available
}
}
}
// 网格列表 - 2列(与草稿箱一样放在内容区域内部滚动)
LazyVerticalGrid(
columns = GridCells.Fixed(2),
modifier = Modifier
.fillMaxWidth()
.weight(1f)
.nestedScroll(nestedScrollConnection),
contentPadding = PaddingValues(
start = 8.dp,
top = 8.dp,
end = 8.dp,
bottom = 8.dp
),
horizontalArrangement = Arrangement.spacedBy(10.dp),
verticalArrangement = Arrangement.spacedBy(10.dp)
) {
itemsIndexed(ZODIAC_SIGN_RES_IDS) { index, zodiacResId ->
val zodiacText = stringResource(zodiacResId)
ZodiacItem(
zodiac = zodiacText,
zodiacResId = zodiacResId,
isSelected = zodiacResId == currentZodiacResId,
onClick = {
model.zodiac = zodiacText
onClose()
// 网格列表 - 2列
LazyVerticalGrid(
columns = GridCells.Fixed(2),
modifier = Modifier
.fillMaxWidth()
.weight(1f)
.nestedScroll(nestedScrollConnection),
contentPadding = PaddingValues(
start = 8.dp,
top = 8.dp,
end = 8.dp,
bottom = 8.dp
),
horizontalArrangement = Arrangement.spacedBy(10.dp),
verticalArrangement = Arrangement.spacedBy(10.dp)
) {
itemsIndexed(ZODIAC_SIGN_RES_IDS) { index, zodiacResId ->
val zodiacText = stringResource(zodiacResId)
ZodiacItem(
zodiac = zodiacText,
zodiacResId = zodiacResId,
isSelected = zodiacResId == currentZodiacResId,
onClick = {
// 保存当前语言的星座文本
model.zodiac = zodiacText
// 立即保存到本地存储,确保选择后立即生效
AppState.UserId?.let { uid ->
com.aiosman.ravenow.AppStore.setUserZodiac(uid, zodiacText)
}
)
}
onClose()
}
)
}
}
}
@@ -335,9 +340,9 @@ fun ZodiacItem(
Column(
modifier = Modifier
.fillMaxWidth()
.aspectRatio(1.1f)
.aspectRatio(1.1f) // 增加宽高比,使高度相对更低
.shadow(
elevation = if (isDarkMode) 8.dp else 2.dp,
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)
)
@@ -349,27 +354,30 @@ fun ZodiacItem(
) {
onClick()
}
.padding(horizontal = 16.dp, vertical = 12.dp),
.padding(horizontal = 24.dp, vertical = 12.dp), // 减小垂直padding确保文本不被遮挡
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
// 直接把图标和文字放在灰色卡片内部,不再额外嵌套一层 Box
Image(
painter = painterResource(id = getZodiacImageResId(zodiacResId)),
contentDescription = zodiac,
// 图标稍微放大一些,让视觉更聚焦在星座图标上
modifier = Modifier.size(96.dp)
)
Spacer(modifier = Modifier.height(0.dp))
// 星座图标 - 使用对应星座的图片
Box(
modifier = Modifier.size(100.dp),
contentAlignment = Alignment.Center
) {
Image(
painter = painterResource(id = getZodiacImageResId(zodiacResId)),
contentDescription = zodiac,
modifier = Modifier.size(100.dp)
)
}
// 星座名称 - 使用负间距让文本向上移动,与图标更靠近
Text(
text = zodiac,
fontSize = 14.sp,
fontWeight = FontWeight.Medium,
color = appColors.text,
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.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.aiosman.ravenow.AppState
import com.aiosman.ravenow.LocalAppTheme
import com.aiosman.ravenow.LocalNavController
import com.aiosman.ravenow.R
@@ -70,19 +68,6 @@ fun ChangePasswordScreen() {
var confirmPasswordError by remember { mutableStateOf<String?>(null) }
var passwordError by remember { mutableStateOf<String?>(null) }
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 {
// 使用通用密码校验器校验当前密码
val currentPasswordValidation = PasswordValidator.validateCurrentPassword(currentPassword, context)
@@ -127,9 +112,7 @@ fun ChangePasswordScreen() {
password = true,
label = stringResource(R.string.current_password),
hint = stringResource(R.string.current_password_tip5),
error = oldPasswordError,
customHintColor = hintColor,
customLabelColor = labelColor
error = oldPasswordError
)
Spacer(modifier = Modifier.height(4.dp))
TextInputField(
@@ -138,9 +121,7 @@ fun ChangePasswordScreen() {
password = true,
label = stringResource(R.string.new_password),
hint = stringResource(R.string.new_password),
error = passwordError,
customHintColor = hintColor,
customLabelColor = labelColor
error = passwordError
)
Spacer(modifier = Modifier.height(4.dp))
TextInputField(
@@ -149,9 +130,7 @@ fun ChangePasswordScreen() {
password = true,
label = stringResource(R.string.confirm_new_password_tip1),
hint = stringResource(R.string.new_password_tip1),
error = confirmPasswordError,
customHintColor = hintColor,
customLabelColor = labelColor
error = confirmPasswordError
)
Spacer(modifier = Modifier.height(50.dp))
ActionButton(

View File

@@ -70,8 +70,6 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.graphics.Brush
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.ui.composables.pickupAndCompressLauncher
import android.widget.Toast
@@ -79,8 +77,6 @@ import java.io.File
import androidx.activity.compose.BackHandler
import com.aiosman.ravenow.ui.account.ZodiacBottomSheetHost
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()
// 挂载MBTI选择弹窗
MbtiBottomSheetHost()
Box(
modifier = Modifier
@@ -398,7 +392,7 @@ fun AccountEditScreen2(onUpdateBanner: ((Uri, File, Context) -> Unit)? = null,)
ProfileInfoCard(
label = stringResource(R.string.nickname),
value = model.name,
placeholder = stringResource(R.string.nickname_placeholder),
placeholder = "Value",
onValueChange = { onNicknameChange(it) },
isMultiline = false
)
@@ -409,7 +403,7 @@ fun AccountEditScreen2(onUpdateBanner: ((Uri, File, Context) -> Unit)? = null,)
ProfileInfoCard(
label = stringResource(R.string.personal_intro),
value = model.bio,
placeholder = "",
placeholder = "Welcome to my fantiac word i will show you something about magic",
onValueChange = { onBioChange(it) },
isMultiline = true
)
@@ -431,7 +425,9 @@ fun AccountEditScreen2(onUpdateBanner: ((Uri, File, Context) -> Unit)? = null,)
iconResDark = null, // TODO: 添加MBTI暗色模式图标
iconResLight = null, // TODO: 添加MBTI亮色模式图标
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.isUpdating = true
try {
model.updateUserProfile(context)
model.viewModelScope.launch(Dispatchers.Main) {
debouncedNavigation {
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.updateUserProfile(context)
model.viewModelScope.launch(Dispatchers.Main) {
debouncedNavigation {
navController.navigateUp()
}
model.isUpdating = false
}
}
},
@@ -566,45 +550,29 @@ fun ProfileInfoCard(
isMultiline: Boolean = false
) {
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(
modifier = Modifier
.fillMaxWidth()
.height(if (isMultiline) 66.dp else 56.dp) // 昵称框高度56dp个人简介66dp
.clip(RoundedCornerShape(16.dp))
.background(appColors.secondaryBackground),
contentAlignment = if (isMultiline && lineCount > 1) Alignment.TopStart else Alignment.CenterStart
contentAlignment = if (isMultiline) Alignment.TopStart else Alignment.CenterStart
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp)
.padding(vertical = if (isMultiline && lineCount > 1) 11.dp else 0.dp),
verticalAlignment = verticalAlignment
.padding(vertical = if (isMultiline) 11.dp else 0.dp),
verticalAlignment = if (isMultiline) Alignment.Top else Alignment.CenterVertically
) {
// 标签
Box(
modifier = Modifier
.width(100.dp)
.height(if (isMultiline) 44.dp else 56.dp),
contentAlignment = Alignment.CenterStart
) {
Text(
text = label,
fontSize = 17.sp,
fontWeight = FontWeight.Normal,
color = appColors.text
)
}
Text(
text = label,
fontSize = 17.sp,
fontWeight = FontWeight.Normal,
color = appColors.text,
modifier = Modifier.width(100.dp)
)
Spacer(modifier = Modifier.width(16.dp))
@@ -612,26 +580,10 @@ fun ProfileInfoCard(
Box(
modifier = Modifier.weight(1f)
) {
// 对于个人简介isMultiline = true当值为空且没有焦点时显示图标
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 文字
if (value.isEmpty()) {
Text(
text = placeholder,
fontSize = 17.sp,
fontSize = if (isMultiline) 15.sp else 17.sp,
fontWeight = FontWeight.Normal,
color = appColors.secondaryText,
modifier = Modifier.fillMaxWidth()
@@ -641,11 +593,7 @@ fun ProfileInfoCard(
BasicTextField(
value = value,
onValueChange = onValueChange,
modifier = Modifier
.fillMaxWidth()
.onFocusChanged { focusState ->
isFocused = focusState.isFocused
},
modifier = Modifier.fillMaxWidth(),
textStyle = androidx.compose.ui.text.TextStyle(
fontSize = if (isMultiline) 15.sp else 17.sp,
fontWeight = FontWeight.Normal,
@@ -653,12 +601,7 @@ fun ProfileInfoCard(
),
cursorBrush = SolidColor(appColors.text),
maxLines = if (isMultiline) Int.MAX_VALUE else 1,
singleLine = !isMultiline,
onTextLayout = { textLayoutResult: TextLayoutResult ->
if (isMultiline) {
lineCount = textLayoutResult.lineCount
}
}
singleLine = !isMultiline
)
}
}

View File

@@ -17,7 +17,6 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
@@ -50,14 +49,6 @@ fun RemoveAccountScreen() {
var passwordError by remember { mutableStateOf<String?>(null) }
val scope = rememberCoroutineScope()
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) {
// 使用通用密码校验器
@@ -141,8 +132,7 @@ fun RemoveAccountScreen() {
},
password = true,
hint = stringResource(R.string.remove_account_password_hint),
error = passwordError,
customHintColor = hintColor
error = passwordError
)
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.entity.AccountProfileEntity
import com.aiosman.ravenow.entity.ChatItem
import com.aiosman.ravenow.im.OpenIMManager
import io.openim.android.sdk.OpenIMClient
import io.openim.android.sdk.enums.ConversationType
import io.openim.android.sdk.enums.ViewType
@@ -100,6 +101,10 @@ abstract class BaseChatViewModel : ViewModel() {
override fun onSuccess(data: ConversationInfo) {
conversationID = data.conversationID
// 如果是群组的会话id应该加上s修正,不知道是不是openIm的bug
if (data.conversationType == 2) {
conversationID = "s${conversationID}"
}
Log.d(getLogTag(), "获取会话信息成功conversationID: $conversationID")
onSuccess?.invoke()
}
@@ -324,6 +329,7 @@ abstract class BaseChatViewModel : ViewModel() {
OpenIMClient.getInstance().messageManager.getAdvancedHistoryMessageList(
object : OnBase<AdvancedMessage> {
override fun onSuccess(data: AdvancedMessage?) {
val messages = data?.messageList ?: emptyList()
val newChatItems = messages.mapNotNull {
ChatItem.convertToChatItem(it, context, avatar = getMessageAvatar(it))

View File

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

View File

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

View File

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

View File

@@ -1,13 +1,11 @@
package com.aiosman.ravenow.ui.chat
import android.content.Context
import android.util.Base64
import android.util.Log
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.viewModelScope
import com.aiosman.ravenow.AppStore
import com.aiosman.ravenow.data.api.ApiClient
import com.aiosman.ravenow.data.api.SendChatAiRequestBody
import io.openim.android.sdk.enums.ConversationType
@@ -52,46 +50,17 @@ class GroupChatViewModel(
}
private suspend fun getGroupInfo() {
try {
val response = ApiClient.api.getRoomDetail(trtcId = groupId)
val room = response.body()?.data
groupInfo = room?.let {
GroupInfo(
groupId = groupId,
groupName = it.name,
groupAvatar = if (it.avatar.isNullOrEmpty()) {
val groupIdBase64 = Base64.encodeToString(
groupId.toByteArray(),
Base64.NO_WRAP
)
"${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
}
// 简化群组信息获取,使用默认信息
groupInfo = GroupInfo(
groupId = groupId,
groupName = name,
groupAvatar = avatar,
memberCount = 0,
ownerId = ""
)
groupName = groupInfo?.groupName ?: ""
groupAvatar = groupInfo?.groupAvatar ?: ""
memberCount = groupInfo?.memberCount ?: 0
}
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.navigationBars
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.HorizontalDivider
@@ -30,11 +28,9 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalDensity
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.unit.dp
import androidx.compose.ui.unit.sp
import kotlinx.coroutines.launch
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope
@@ -84,9 +79,8 @@ class CommentModalViewModel(
fun CommentModalContent(
postId: Int? = null,
commentCount: Int = 0,
onDismiss: () -> Unit = {},
showTitle: Boolean = true,
onCommentAdded: () -> Unit = {}
onCommentAdded: () -> Unit = {},
onDismiss: () -> Unit = {}
) {
val model = viewModel<CommentModalViewModel>(
key = "CommentModalViewModel_$postId",
@@ -110,8 +104,6 @@ fun CommentModalContent(
var softwareKeyboardController = LocalSoftwareKeyboardController.current
var replyComment by remember { mutableStateOf<CommentEntity?>(null) }
var shouldAutoFocus by remember { mutableStateOf(false) }
val listState = rememberLazyListState()
val scope = rememberCoroutineScope()
LaunchedEffect(imePadding) {
bottomPadding = imePadding.dp
@@ -169,42 +161,28 @@ fun CommentModalContent(
modifier = Modifier
.fillMaxSize()
) {
// 拖动手柄
Box(
modifier = Modifier
.fillMaxWidth()
.padding(top = 8.dp, bottom = 12.dp),
contentAlignment = Alignment.Center
.padding(start = 16.dp, bottom = 16.dp, end = 16.dp)
) {
Box(
modifier = Modifier
.width(40.dp)
.height(4.dp)
.clip(RoundedCornerShape(50))
.background(AppColors.divider)
Text(
stringResource(R.string.comment),
fontSize = 18.sp,
fontWeight = FontWeight.Bold,
color = AppColors.text,
modifier = Modifier.align(Alignment.Center)
)
}
if (showTitle) {
Box(
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)
)
}
}
HorizontalDivider(
color = AppColors.divider
)
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 20.dp, vertical = 8.dp),
.padding(horizontal = 24.dp, vertical = 16.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
@@ -213,14 +191,9 @@ fun CommentModalContent(
fontSize = 14.sp,
color = AppColors.secondaryText
)
OrderSelectionComponent(
selectedOrder = commentViewModel.order
) {
OrderSelectionComponent {
commentViewModel.order = it
commentViewModel.reloadComment()
scope.launch {
listState.scrollToItem(0)
}
}
}
Box(
@@ -231,8 +204,7 @@ fun CommentModalContent(
LazyColumn(
modifier = Modifier
.fillMaxSize()
.padding(horizontal = 16.dp),
state = listState
.padding(horizontal = 16.dp)
) {
item {
CommentContent(

View File

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

View File

@@ -36,7 +36,6 @@ fun StatusBarMaskLayout(
modifier: Modifier = Modifier,
darkIcons: Boolean = true,
useNavigationBarMask: Boolean = true,
includeStatusBarPadding: Boolean = true,
maskBoxBackgroundColor: Color = Color.Transparent,
content: @Composable ColumnScope.() -> Unit
) {
@@ -51,13 +50,13 @@ fun StatusBarMaskLayout(
Column(
modifier = modifier.fillMaxSize()
) {
if (includeStatusBarPadding) {
Box(
modifier = Modifier
.height(paddingValues.calculateTopPadding())
.fillMaxWidth()
.background(maskBoxBackgroundColor)
)
Box(
modifier = Modifier
.height(paddingValues.calculateTopPadding())
.fillMaxWidth()
.background(maskBoxBackgroundColor)
) {
}
content()
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.verticalScroll
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.pullRefresh
import androidx.compose.material.pullrefresh.rememberPullRefreshState
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
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.platform.LocalContext
import androidx.compose.ui.res.painterResource
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.modifiers.noRippleClickable
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
@OptIn(ExperimentalMaterialApi::class)
@@ -88,11 +84,43 @@ fun FavouriteListPage() {
var moments = dataFlow.collectAsLazyPagingItems()
if (!isNetworkAvailable) {
NetworkErrorContent(
onReload = {
model.refreshPager(force = true)
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.secondaryText,
fontSize = 14.sp,
fontWeight = FontWeight.W400
)
Spacer(modifier = Modifier.height(16.dp))
ReloadButton(
onClick = {
model.refreshPager(force = true)
}
)
}
)
}
} else if(moments.itemCount == 0) {
Box(
modifier = Modifier
@@ -159,10 +187,7 @@ fun FavouriteListPage() {
.clip(RoundedCornerShape(8.dp)),
context = context
)
val isVideoMoment = momentItem.images.isEmpty() && !momentItem.videos.isNullOrEmpty()
if (momentItem.images.size > 1 || (momentItem.videos?.size ?: 0) > 1) {
if (momentItem.images.size > 1) {
Box(
modifier = Modifier
.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 kotlinx.coroutines.launch
import com.aiosman.ravenow.utils.NetworkUtils
import com.aiosman.ravenow.ui.network.NetworkErrorContent
import com.aiosman.ravenow.ui.network.ReloadButton
@OptIn(ExperimentalMaterialApi::class)
@Composable
@@ -76,11 +76,43 @@ fun FollowerListScreen(userId: Int) {
val isNetworkAvailable = NetworkUtils.isNetworkAvailable(LocalContext.current)
if (!isNetworkAvailable) {
NetworkErrorContent(
onReload = {
model.loadData(userId, true)
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))
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) {
Box(
modifier = Modifier

View File

@@ -21,7 +21,6 @@ import androidx.compose.runtime.rememberCoroutineScope
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.platform.LocalContext
import androidx.compose.ui.res.painterResource
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.StatusBarMaskLayout
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 com.aiosman.ravenow.utils.NetworkUtils
@@ -49,14 +48,13 @@ import com.aiosman.ravenow.utils.NetworkUtils
* 关注消息列表
*/
@Composable
fun FollowerNoticeScreen(includeStatusBarPadding: Boolean = true) {
fun FollowerNoticeScreen() {
val scope = rememberCoroutineScope()
val AppColors = LocalAppTheme.current
StatusBarMaskLayout(
modifier = Modifier.background(color = AppColors.background).padding(horizontal = 16.dp),
darkIcons = !AppState.darkMode,
maskBoxBackgroundColor = AppColors.background,
includeStatusBarPadding = includeStatusBarPadding
maskBoxBackgroundColor = AppColors.background
) {
val model = FollowerNoticeViewModel
var dataFlow = model.followerItemsFlow
@@ -68,11 +66,43 @@ fun FollowerNoticeScreen(includeStatusBarPadding: Boolean = true) {
val isNetworkAvailable = NetworkUtils.isNetworkAvailable(LocalContext.current)
if (!isNetworkAvailable) {
NetworkErrorContent(
onReload = {
model.reload(force = true)
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))
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) {
Box(
modifier = Modifier
@@ -141,57 +171,35 @@ fun FollowItem(
val AppColors = LocalAppTheme.current
val context = LocalContext.current
val navController = LocalNavController.current
val followText = stringResource(R.string.followed_you)
Row(
Box(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 12.dp),
verticalAlignment = Alignment.CenterVertically
) {
// 左侧头像区域
CustomAsyncImage(
context = context,
imageUrl = avatar,
contentDescription = nickname,
modifier = Modifier
.size(40.dp)
.clip(CircleShape)
.noRippleClickable {
navController.navigate(
NavigationRoute.AccountProfile.route.replace(
"{id}",
userId.toString()
)
.padding(vertical = 16.dp)
.noRippleClickable {
navController.navigate(
NavigationRoute.AccountProfile.route.replace(
"{id}",
userId.toString()
)
}
)
// 右侧内容区域
)
}
) {
Row(
modifier = Modifier
.weight(1f)
.padding(start = 8.dp),
verticalAlignment = Alignment.CenterVertically
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
) {
CustomAsyncImage(
context = context,
imageUrl = avatar,
contentDescription = nickname,
modifier = Modifier
.size(40.dp)
.clip(CircleShape)
)
Spacer(modifier = Modifier.width(12.dp))
Column(
modifier = Modifier.weight(1f)
) {
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
)
Text(nickname, fontWeight = FontWeight.Bold, fontSize = 16.sp, color = AppColors.text)
}
if (!isFollowing && userId != AppState.UserId) {
FollowButton(

View File

@@ -40,7 +40,7 @@ import com.aiosman.ravenow.exp.viewModelFactory
import com.aiosman.ravenow.ui.NavigationRoute
import com.aiosman.ravenow.ui.comment.NoticeScreenHeader
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 com.aiosman.ravenow.utils.NetworkUtils
@@ -78,11 +78,43 @@ fun FollowingListScreen(userId: Int) {
val isNetworkAvailable = NetworkUtils.isNetworkAvailable(LocalContext.current)
if (!isNetworkAvailable) {
NetworkErrorContent(
onReload = {
model.loadData(userId, true)
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))
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) {
Box(
modifier = Modifier

View File

@@ -99,15 +99,6 @@ fun CreateGroupChatScreen() {
}
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)
@@ -504,134 +495,63 @@ fun CreateGroupChatScreen() {
}
}
// 创建群聊按钮 - 固定在底部(启用时使用渐变背景)
val isCreateEnabled =
groupName.text.isNotEmpty() && selectedMembers.isNotEmpty() && !CreateGroupChatViewModel.isLoading
Box(
modifier = Modifier
.fillMaxWidth()
.padding(
start = 16.dp,
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
// 创建群聊按钮 - 固定在底部
Button(
onClick = {
// 创建群聊逻辑
if (selectedMembers.isNotEmpty()) {
// 检查是否超过上限
if (selectedMembers.size > maxMemberLimit) {
CreateGroupChatViewModel.showError(context.getString(R.string.create_group_chat_exceed_limit, maxMemberLimit))
return@Button
}
}
) {
Button(
onClick = {
// 创建群聊逻辑
if (selectedMembers.isNotEmpty()) {
// 检查是否超过上限
if (selectedMembers.size > maxMemberLimit) {
CreateGroupChatViewModel.showError(
context.getString(
R.string.create_group_chat_exceed_limit,
maxMemberLimit
)
// 如果费用大于0显示确认弹窗
if (cost > 0) {
CreateGroupChatViewModel.showConfirmDialog()
} else {
// 费用为0直接创建
scope.launch {
val success = CreateGroupChatViewModel.createGroupChat(
groupName = groupName.text,
selectedMembers = selectedMembers,
context = context
)
return@Button
}
// 如果费用大于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()
}
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
)
}
}
// 禁用状态下拦截点击并弹出提示
if (!isCreateEnabled) {
Box(
modifier = Modifier
.matchParentSize()
.noRippleClickable {
showSelectTipsDialog = true
}
},
modifier = Modifier
.fillMaxWidth()
.padding(start = 16.dp, end = 16.dp, top = buttonTopPadding, bottom = navigationBarPadding + 16.dp),
colors = ButtonDefaults.buttonColors(
containerColor = AppColors.main,
contentColor = AppColors.mainText,
disabledContainerColor = AppColors.disabledBackground,
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) {
CreateGroupChatConfirmDialog(

View File

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

View File

@@ -107,16 +107,16 @@ fun GroupMembersScreen(groupId: String) {
textAlign = TextAlign.Center
)
// androidx.compose.foundation.Image(
// painter = painterResource(R.drawable.rider_pro_add_other),
// contentDescription = stringResource(R.string.group_chat_info_add_member),
// colorFilter = ColorFilter.tint(AppColors.text),
// modifier = Modifier
// .size(24.dp)
// .noRippleClickable {
// navController.navigateToAddGroupMember(groupId, viewModel.groupInfo?.groupName)
// }
// )
androidx.compose.foundation.Image(
painter = painterResource(R.drawable.rider_pro_add_other),
contentDescription = stringResource(R.string.group_chat_info_add_member),
colorFilter = ColorFilter.tint(AppColors.text),
modifier = Modifier
.size(24.dp)
.noRippleClickable {
navController.navigateToAddGroupMember(groupId, viewModel.groupInfo?.groupName)
}
)
}
}
@@ -391,7 +391,7 @@ private fun MemberItem(
Spacer(modifier = Modifier.weight(1f))
// 菜单按钮
if (isAdmin) {
IconButton(
onClick = { onMenuClick(itemPosition, itemHeight) },
modifier = Modifier.size(24.dp)
@@ -404,7 +404,6 @@ private fun MemberItem(
.size(24.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.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.tween
import androidx.compose.foundation.ExperimentalFoundationApi
@@ -136,7 +140,6 @@ fun IndexScreen() {
ModalNavigationDrawer(
drawerState = drawerState,
gesturesEnabled = drawerState.isOpen,
scrimColor = Color.Black.copy(alpha = 0.6f),
drawerContent = {
CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Ltr) {
SideMenuContent(
@@ -522,6 +525,8 @@ fun SideMenuContent(
} else {
Color(0xFFFAF9FB) // 亮色模式:浅灰色
}
// 遮罩颜色 黑色透明度0.6
val overlayColor = Color.Black.copy(alpha = 0.6f)
// 卡片背景色 - 根据暗色模式适配
val cardBackgroundColor = if (darkModeEnabled) {
appColors.background // 暗色模式:深色背景
@@ -541,6 +546,24 @@ fun SideMenuContent(
modifier = Modifier
.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(
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.height
import androidx.compose.foundation.layout.navigationBars
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.systemBars
@@ -24,22 +23,18 @@ import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Icon
import androidx.compose.material.Text
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Scaffold
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@@ -63,33 +58,24 @@ import com.aiosman.ravenow.LocalNavController
import com.aiosman.ravenow.R
import com.aiosman.ravenow.ui.NavigationRoute
import com.aiosman.ravenow.ui.composables.CustomAsyncImage
import com.aiosman.ravenow.ui.index.tabs.ai.tabs.mine.MineAgent
import com.aiosman.ravenow.ui.index.tabs.ai.tabs.hot.HotAgent
import com.aiosman.ravenow.ui.modifiers.noRippleClickable
import com.aiosman.ravenow.ui.composables.TabItem
import com.aiosman.ravenow.ui.composables.TabSpacer
import com.aiosman.ravenow.ui.index.tabs.moment.CustomTabItem
import com.aiosman.ravenow.ui.index.tabs.moment.tabs.expolre.AgentItem
import com.aiosman.ravenow.ui.index.tabs.moment.tabs.expolre.ExploreViewModel
import com.aiosman.ravenow.utils.DebounceUtils
import com.aiosman.ravenow.utils.ResourceCleanupManager
import kotlinx.coroutines.launch
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
import androidx.compose.foundation.lazy.grid.items as gridItems
import androidx.compose.material.CircularProgressIndicator
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.graphics.Brush
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.ui.platform.LocalConfiguration
import com.airbnb.lottie.compose.LottieAnimation
import com.airbnb.lottie.compose.LottieCompositionSpec
@@ -113,10 +99,6 @@ fun Agent() {
val navigationBarPaddings =
WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() + 48.dp
val statusBarPaddingValues = WindowInsets.systemBars.asPaddingValues()
// 游客模式下只显示热门Agent正常用户显示我的Agent和热门Agent
val tabCount = if (AppStore.isGuest) 1 else 2
var pagerState = rememberPagerState { tabCount }
var scope = rememberCoroutineScope()
val viewModel: AgentViewModel = AgentViewModel
@@ -125,16 +107,6 @@ fun Agent() {
viewModel.ensureDataLoaded()
}
// 防抖状态
var lastClickTime by remember { mutableStateOf(0L) }
// 页面退出时只清理必要的资源不清理推荐Agent数据
DisposableEffect(Unit) {
onDispose {
// 只清理子页面的资源保留推荐Agent数据
// ResourceCleanupManager.cleanupPageResources("ai")
}
}
val agentItems = viewModel.agentItems
var selectedTabIndex by remember { mutableStateOf(0) }
@@ -165,7 +137,7 @@ fun Agent() {
contentDescription = "Rave AI Logo",
modifier = Modifier
.height(44.dp)
.padding(top =9.dp,bottom=9.dp)
.padding(top = 9.dp, bottom = 9.dp)
.wrapContentSize(),
// colorFilter = ColorFilter.tint(AppColors.text)
)
@@ -176,7 +148,7 @@ fun Agent() {
contentDescription = "search",
modifier = Modifier
.size(44.dp)
.padding(top = 9.dp,bottom=9.dp)
.padding(top = 9.dp, bottom = 9.dp)
.noRippleClickable {
navController.navigate(NavigationRoute.Search.route)
},
@@ -267,11 +239,19 @@ fun Agent() {
) {
when {
selectedTabIndex == 0 -> {
AgentViewPagerSection(agentItems = viewModel.agentItems.take(15), viewModel)
AgentViewPagerSection(
agentItems = viewModel.agentItems.take(15),
viewModel
)
}
selectedTabIndex in 1..viewModel.categories.size -> {
AgentViewPagerSection(agentItems = viewModel.agentItems.take(15), viewModel)
AgentViewPagerSection(
agentItems = viewModel.agentItems.take(15),
viewModel
)
}
else -> {
val shuffledAgents = viewModel.agentItems.shuffled().take(15)
AgentViewPagerSection(agentItems = shuffledAgents, viewModel)
@@ -329,133 +309,81 @@ fun Agent() {
}
// 只有当热门聊天室有数据时,才展示“发现更多”区域
if (viewModel.chatRooms.isNotEmpty()) {
item { Spacer(modifier = Modifier.height(20.dp)) }
item { Spacer(modifier = Modifier.height(20.dp)) }
// "发现更多" 标题 - 吸顶
stickyHeader(key = "discover_more") {
Row(
modifier = Modifier
.fillMaxWidth()
.background(AppColors.background)
.padding(top = 8.dp, bottom = 12.dp),
horizontalArrangement = Arrangement.Start,
verticalAlignment = Alignment.Bottom
) {
Image(
painter = painterResource(R.mipmap.bars_x_buttons_home_n_copy_2),
contentDescription = "agent",
modifier = Modifier.size(28.dp)
)
Spacer(modifier = Modifier.width(4.dp))
androidx.compose.material3.Text(
text = stringResource(R.string.agent_find),
fontSize = 16.sp,
fontWeight = androidx.compose.ui.text.font.FontWeight.W900,
color = AppColors.text
)
}
// "发现更多" 标题 - 吸顶
stickyHeader(key = "discover_more") {
Row(
modifier = Modifier
.fillMaxWidth()
.background(AppColors.background)
.padding(top = 8.dp, bottom = 12.dp),
horizontalArrangement = Arrangement.Start,
verticalAlignment = Alignment.Bottom
) {
Image(
painter = painterResource(R.mipmap.bars_x_buttons_home_n_copy_2),
contentDescription = "agent",
modifier = Modifier.size(28.dp)
)
Spacer(modifier = Modifier.width(4.dp))
androidx.compose.material3.Text(
text = stringResource(R.string.agent_find),
fontSize = 16.sp,
fontWeight = androidx.compose.ui.text.font.FontWeight.W900,
color = AppColors.text
)
}
}
// Agent网格 - 使用行式布局
items(
items = agentItems.chunked(2),
key = { row -> row.firstOrNull()?.openId ?: "" }
) { rowItems ->
Row(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 16.dp),
horizontalArrangement = Arrangement.spacedBy(16.dp)
) {
rowItems.forEach { agentItem ->
Box(
modifier = Modifier.weight(1f)
) {
AgentCardSquare(
agentItem = agentItem,
viewModel = viewModel,
navController = LocalNavController.current
)
}
}
// 如果这一行只有一个item添加一个空的占位符
if (rowItems.size == 1) {
Spacer(modifier = Modifier.weight(1f))
}
}
}
// 加载更多指示器(仅在展示"发现更多"时显示)
if (viewModel.isLoadingMore) {
item {
// Agent网格 - 使用行式布局
items(
items = agentItems.chunked(2),
key = { row -> row.firstOrNull()?.openId ?: "" }
) { rowItems ->
Row(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 16.dp),
horizontalArrangement = Arrangement.spacedBy(16.dp)
) {
rowItems.forEach { agentItem ->
Box(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 24.dp),
contentAlignment = Alignment.Center
modifier = Modifier.weight(1f)
) {
LottieAnimation(
composition = rememberLottieComposition(LottieCompositionSpec.Asset("star_Loader.lottie")).value,
iterations = LottieConstants.IterateForever,
modifier = Modifier.size(80.dp)
AgentCardSquare(
agentItem = agentItem,
viewModel = viewModel,
navController = LocalNavController.current
)
}
}
// 如果这一行只有一个item添加一个空的占位符
if (rowItems.size == 1) {
Spacer(modifier = Modifier.weight(1f))
}
}
}
}
}
}
@Composable
fun AgentGridLayout(
agentItems: List<AgentItem>,
viewModel: AgentViewModel,
navController: NavHostController
) {
Column(
modifier = Modifier.fillMaxWidth()
) {
// 将agentItems按两列分组
agentItems.chunked(2).forEachIndexed { rowIndex, rowItems ->
Row(
modifier = Modifier
.fillMaxWidth()
.padding(
top = if (rowIndex == 0) 30.dp else 20.dp, // 第一行添加更多顶部间距
bottom = 20.dp
),
horizontalArrangement = Arrangement.spacedBy(16.dp)
) {
// 第一列
Box(
modifier = Modifier.weight(1f)
) {
AgentCardSquare(
agentItem = rowItems[0],
viewModel = viewModel,
navController = navController
)
}
// 第二列(如果存在)
if (rowItems.size > 1) {
// 加载更多指示器(仅在展示"发现更多"时显示)
if (viewModel.isLoadingMore) {
item {
Box(
modifier = Modifier.weight(1f)
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 24.dp),
contentAlignment = Alignment.Center
) {
AgentCardSquare(
agentItem = rowItems[1],
viewModel = viewModel,
navController = navController
LottieAnimation(
composition = rememberLottieComposition(LottieCompositionSpec.Asset("star_Loader.lottie")).value,
iterations = LottieConstants.IterateForever,
modifier = Modifier.size(80.dp)
)
}
} else {
// 如果只有一列,添加空白占位
Spacer(modifier = Modifier.weight(1f))
}
}
}
}
}
@@ -486,11 +414,11 @@ fun AgentCardSquare(
}
) {
// 背景大图
CustomAsyncImage(
imageUrl = agentItem.avatar,
CustomAsyncImage(
imageUrl = agentItem.avatar,
contentDescription = agentItem.title,
modifier = Modifier.fillMaxSize(),
contentScale = androidx.compose.ui.layout.ContentScale.Crop,
contentScale = androidx.compose.ui.layout.ContentScale.Crop,
defaultRes = R.mipmap.rider_pro_agent
)
@@ -507,27 +435,27 @@ fun AgentCardSquare(
)
.padding(12.dp)
) {
Column(
modifier = Modifier
.fillMaxWidth()
Column(
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 40.dp) // 为底部聊天按钮预留空间
) {
androidx.compose.material3.Text(
text = agentItem.title,
) {
androidx.compose.material3.Text(
text = agentItem.title,
color = Color.White,
fontSize = 14.sp,
fontSize = 14.sp,
fontWeight = androidx.compose.ui.text.font.FontWeight.W700,
maxLines = 1,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
)
Spacer(modifier = Modifier.height(4.dp))
androidx.compose.material3.Text(
text = agentItem.desc,
androidx.compose.material3.Text(
text = agentItem.desc,
color = Color.White.copy(alpha = 0.92f),
fontSize = 11.sp,
maxLines = 2,
maxLines = 2,
overflow = TextOverflow.Ellipsis
)
)
}
}
@@ -562,9 +490,10 @@ fun AgentCardSquare(
}
}
}
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun AgentViewPagerSection(agentItems: List<AgentItem>,viewModel: AgentViewModel) {
fun AgentViewPagerSection(agentItems: List<AgentItem>, viewModel: AgentViewModel) {
val AppColors = LocalAppTheme.current
if (agentItems.isEmpty()) return
@@ -711,186 +640,6 @@ fun AgentLargeCard(
}
}
@Composable
fun AgentPage(viewModel: AgentViewModel,agentItems: List<AgentItem>, page: Int, modifier: Modifier = Modifier,navController: NavHostController) {
Column(
modifier = modifier
.fillMaxSize()
.padding(horizontal = 0.dp)
) {
// 显示3个agent
agentItems.forEachIndexed { index, agentItem ->
AgentCard2(agentItem = agentItem, viewModel = viewModel, navController = LocalNavController.current)
if (index < agentItems.size - 1) {
Spacer(modifier = Modifier.height(8.dp))
}
}
}
}
@SuppressLint("SuspiciousIndentation")
@Composable
fun AgentCard2(viewModel: AgentViewModel,agentItem: AgentItem,navController: NavHostController) {
val AppColors = LocalAppTheme.current
// 防抖状态
var lastClickTime by remember { mutableStateOf(0L) }
Row(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 3.dp),
verticalAlignment = Alignment.CenterVertically
) {
// 左侧头像
Box(
modifier = Modifier
.size(48.dp)
.background(Color(0x00F5F5F5), RoundedCornerShape(24.dp))
.clickable {
if (DebounceUtils.simpleDebounceClick(lastClickTime, 500L) {
viewModel.goToProfile(agentItem.openId, navController)
}) {
lastClickTime = System.currentTimeMillis()
}
},
contentAlignment = Alignment.Center
) {
CustomAsyncImage(
imageUrl = agentItem.avatar,
contentDescription = "Agent头像",
modifier = Modifier
.size(48.dp)
.clip(RoundedCornerShape(24.dp)),
contentScale = androidx.compose.ui.layout.ContentScale.Crop,
defaultRes = R.mipmap.group_copy
)
}
Spacer(modifier = Modifier.width(12.dp))
// 中间文字内容
Column(
modifier = Modifier
.weight(1f)
.padding(end = 8.dp)
) {
// 标题
androidx.compose.material3.Text(
text = agentItem.title,
fontSize = 14.sp,
fontWeight = androidx.compose.ui.text.font.FontWeight.W600,
color = AppColors.text,
maxLines = 1,
overflow = androidx.compose.ui.text.style.TextOverflow.Ellipsis
)
Spacer(modifier = Modifier.height(8.dp))
// 描述
androidx.compose.material3.Text(
text = agentItem.desc,
fontSize = 12.sp,
color = AppColors.secondaryText,
maxLines = 1,
overflow = androidx.compose.ui.text.style.TextOverflow.Ellipsis
)
}
// 右侧聊天按钮
Box(
modifier = Modifier
.size(width = 60.dp, height = 32.dp)
.background(
color = Color(0X147c7480),
shape = RoundedCornerShape(
topStart = 14.dp,
topEnd = 14.dp,
bottomStart = 0.dp,
bottomEnd = 14.dp
)
)
.clickable {
if (DebounceUtils.simpleDebounceClick(lastClickTime, 500L) {
// 检查游客模式,如果是游客则跳转登录
if (GuestLoginCheckOut.needLogin(GuestLoginCheckOutScene.CHAT_WITH_AGENT)) {
navController.navigate(NavigationRoute.Login.route)
} else {
viewModel.createSingleChat(agentItem.openId)
viewModel.goToChatAi(
agentItem.openId,
navController = navController
)
}
}) {
lastClickTime = System.currentTimeMillis()
}
},
contentAlignment = Alignment.Center
) {
androidx.compose.material3.Text(
text = stringResource(R.string.chat),
fontSize = 12.sp,
color = AppColors.text,
fontWeight = androidx.compose.ui.text.font.FontWeight.W500
)
}
}
}
@Composable
fun ChatRoomsSection(
chatRooms: List<ChatRoom>,
navController: NavHostController
) {
val AppColors = LocalAppTheme.current
Column(
modifier = Modifier.fillMaxWidth()
) {
// 标题
Row(
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 16.dp),
verticalAlignment = Alignment.CenterVertically
) {
Image(
painter = painterResource(R.mipmap.rider_pro_hot_room),
contentDescription = "chat room",
modifier = Modifier.size(28.dp)
)
Spacer(modifier = Modifier.width(4.dp))
androidx.compose.material3.Text(
text = stringResource(R.string.hot_rooms),
fontSize = 16.sp,
fontWeight = androidx.compose.ui.text.font.FontWeight.W900,
color = AppColors.text
)
}
Column(
modifier = Modifier.fillMaxWidth()
) {
chatRooms.chunked(2).forEach { rowRooms ->
Row(
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 12.dp),
horizontalArrangement = Arrangement.spacedBy(12.dp)
) {
rowRooms.forEach { chatRoom ->
ChatRoomCard(
chatRoom = chatRoom,
navController = navController,
modifier = Modifier.weight(1f)
)
}
}
}
}
}
}
@Composable
fun ChatRoomCard(
chatRoom: ChatRoom,
@@ -938,7 +687,10 @@ fun ChatRoomCard(
.size(cardSize)
.background(AppColors.tabUnselectedBackground, RoundedCornerShape(12.dp))
.clickable(enabled = !viewModel.isJoiningRoom) {
if (!viewModel.isJoiningRoom && DebounceUtils.simpleDebounceClick(lastClickTime, 500L) {
if (!viewModel.isJoiningRoom && DebounceUtils.simpleDebounceClick(
lastClickTime,
500L
) {
// 加入群聊房间
viewModel.joinRoom(
id = chatRoom.id,
@@ -953,7 +705,8 @@ fun ChatRoomCard(
// 处理错误可以显示Toast或其他提示
}
)
}) {
}
) {
lastClickTime = System.currentTimeMillis()
}
}
@@ -967,11 +720,14 @@ fun ChatRoomCard(
modifier = Modifier
.width(cardSize)
.height(120.dp)
.clip(RoundedCornerShape(
topStart = 12.dp,
topEnd = 12.dp,
bottomStart = 0.dp,
bottomEnd = 0.dp)),
.clip(
RoundedCornerShape(
topStart = 12.dp,
topEnd = 12.dp,
bottomStart = 0.dp,
bottomEnd = 0.dp
)
),
contentScale = androidx.compose.ui.layout.ContentScale.Crop,
defaultRes = R.mipmap.rider_pro_agent
)

View File

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

View File

@@ -10,7 +10,6 @@ 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.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
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.modifiers.noRippleClickable
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.LottieCompositionSpec
import com.airbnb.lottie.compose.LottieConstants
import com.airbnb.lottie.compose.rememberLottieComposition
import com.aiosman.ravenow.ui.index.tabs.profile.composable.ChatEmptyStateView
/**
* 智能体聊天列表页面
@@ -91,28 +89,72 @@ fun AgentChatListScreen() {
.pullRefresh(state)
) {
if (AgentChatListViewModel.agentChatList.isEmpty() && !AgentChatListViewModel.isLoading) {
Box(
// 空状态
Column(
modifier = Modifier
.fillMaxSize()
.padding(horizontal = 16.dp),
contentAlignment = Alignment.Center
) {
// 空状态
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.offset(y = (-40).dp)
) {
val isNetworkAvailable = NetworkUtils.isNetworkAvailable(context)
.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
if (isNetworkAvailable) {
ChatEmptyStateView()
} else {
NetworkErrorContentCompact(
onReload = {
AgentChatListViewModel.refreshPager(context = context)
}
)
}
) {
val isNetworkAvailable = NetworkUtils.isNetworkAvailable(context)
if (isNetworkAvailable) {
Spacer(modifier = Modifier.height(39.dp))
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 {

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.modifiers.noRippleClickable
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.ButtonDefaults
import androidx.compose.ui.text.font.FontFamily
import com.aiosman.ravenow.ui.index.tabs.profile.composable.ChatEmptyStateView
data class CombinedConversation(
val type: String, // "agent", "group", or "friend"
@@ -218,31 +217,73 @@ fun AllChatListScreen() {
.pullRefresh(state)
) {
if (allConversations.isEmpty() && !isLoading) {
Box(
Column(
modifier = Modifier
.fillMaxSize()
.padding(horizontal = 16.dp),
contentAlignment = Alignment.Center
.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.offset(y = (-40).dp)
) {
val isNetworkAvailable = NetworkUtils.isNetworkAvailable(context)
val isNetworkAvailable = NetworkUtils.isNetworkAvailable(context)
if (isNetworkAvailable) {
ChatEmptyStateView()
} else {
NetworkErrorContentCompact(
onReload = {
isLoading = true
// 重新加载所有类型的数据
AgentChatListViewModel.refreshPager(context = context)
GroupChatListViewModel.refreshPager(context = context)
FriendChatListViewModel.refreshPager(context = context)
}
)
}
if (isNetworkAvailable) {
Spacer(modifier = Modifier.height(39.dp))
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 = {
isLoading = true
// 重新加载所有类型的数据
AgentChatListViewModel.refreshPager(context = context)
GroupChatListViewModel.refreshPager(context = context)
FriendChatListViewModel.refreshPager(context = context)
}
)
}
}
} else {

View File

@@ -33,7 +33,7 @@ import com.aiosman.ravenow.LocalNavController
import com.aiosman.ravenow.R
import com.aiosman.ravenow.ui.composables.CustomAsyncImage
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.utils.NetworkUtils
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.ButtonDefaults
import androidx.compose.foundation.layout.PaddingValues
import com.aiosman.ravenow.ui.index.tabs.profile.composable.ChatEmptyStateView
@OptIn(ExperimentalMaterialApi::class)
@Composable
@@ -73,27 +72,70 @@ fun FriendChatListScreen() {
.pullRefresh(state)
) {
if (FriendChatListViewModel.friendChatList.isEmpty() && !FriendChatListViewModel.isLoading) {
Box(
Column(
modifier = Modifier
.fillMaxSize()
.padding(horizontal = 16.dp),
contentAlignment = Alignment.Center
.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
//verticalArrangement = Arrangement.Center
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.offset(y = (-40).dp)
) {
val isNetworkAvailable = NetworkUtils.isNetworkAvailable(context)
val isNetworkAvailable = NetworkUtils.isNetworkAvailable(context)
if (isNetworkAvailable) {
ChatEmptyStateView()
} else {
NetworkErrorContentCompact(
onReload = {
FriendChatListViewModel.refreshPager(pullRefresh = true, context = context)
}
)
}
if (isNetworkAvailable) {
Spacer(modifier = Modifier.height(39.dp))
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 = {
FriendChatListViewModel.refreshPager(pullRefresh = true, context = context)
}
)
}
}
} 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.rememberPullRefreshState
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
@@ -27,6 +26,7 @@ import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.graphics.Color
import com.aiosman.ravenow.AppState
import com.aiosman.ravenow.LocalAppTheme
import com.aiosman.ravenow.LocalNavController
@@ -35,8 +35,7 @@ import com.aiosman.ravenow.ui.composables.CustomAsyncImage
import com.aiosman.ravenow.ui.composables.rememberDebouncer
import com.aiosman.ravenow.ui.modifiers.noRippleClickable
import com.aiosman.ravenow.utils.NetworkUtils
import com.aiosman.ravenow.ui.network.NetworkErrorContentCompact
import com.aiosman.ravenow.ui.index.tabs.profile.composable.ChatEmptyStateView
import com.aiosman.ravenow.ui.network.ReloadButton
@OptIn(ExperimentalMaterialApi::class)
@Composable
@@ -68,27 +67,69 @@ fun GroupChatListScreen() {
.pullRefresh(state)
) {
if (GroupChatListViewModel.groupChatList.isEmpty() && !GroupChatListViewModel.isLoading) {
Box(
Column(
modifier = Modifier
.fillMaxSize()
.padding(horizontal = 16.dp),
contentAlignment = Alignment.Center
.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.offset(y = (-40).dp)
) {
val isNetworkAvailable = NetworkUtils.isNetworkAvailable(context)
val isNetworkAvailable = NetworkUtils.isNetworkAvailable(context)
if (isNetworkAvailable) {
ChatEmptyStateView()
} else {
NetworkErrorContentCompact(
onReload = {
GroupChatListViewModel.refreshPager(context = context)
}
)
}
if (isNetworkAvailable) {
Spacer(modifier = Modifier.height(39.dp))
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 = {
GroupChatListViewModel.refreshPager(context = context)
}
)
}
}
} 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()) {
@@ -172,16 +206,16 @@ fun GroupChatItem(
val AppColors = LocalAppTheme.current
val chatDebouncer = rememberDebouncer()
val avatarDebouncer = rememberDebouncer()
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 24.dp, vertical = 12.dp)
.padding(horizontal = 16.dp, vertical = 12.dp)
.noRippleClickable {
chatDebouncer {
onChatClick(conversation)
}
}
},
verticalAlignment = Alignment.CenterVertically
) {
Box {
CustomAsyncImage(
@@ -201,9 +235,9 @@ fun GroupChatItem(
Column(
modifier = Modifier
.weight(1f)
.padding(start = 12.dp)
.padding(start = 12.dp, top = 2.dp),
verticalArrangement = Arrangement.Center
) {
Row(
modifier = Modifier.fillMaxWidth(),
@@ -211,22 +245,22 @@ fun GroupChatItem(
) {
Text(
text = conversation.groupName,
fontSize = 16.sp,
fontSize = 14.sp,
fontWeight = FontWeight.Bold,
color = AppColors.text,
modifier = Modifier.weight(1f)
)
Spacer(modifier = Modifier.width(8.dp))
Spacer(modifier = Modifier.width(6.dp))
Text(
text = conversation.lastMessageTime,
fontSize = 12.sp,
fontSize = 11.sp,
color = AppColors.secondaryText
)
}
Spacer(modifier = Modifier.height(4.dp))
Spacer(modifier = Modifier.height(6.dp))
Row(
modifier = Modifier.fillMaxWidth(),
@@ -234,29 +268,29 @@ fun GroupChatItem(
) {
Text(
text = "${if (conversation.isSelf) stringResource(R.string.friend_chat_me_prefix) else ""}${conversation.displayText}",
fontSize = 14.sp,
fontSize = 12.sp,
color = AppColors.secondaryText,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
modifier = Modifier.weight(1f)
)
Spacer(modifier = Modifier.width(8.dp))
Spacer(modifier = Modifier.width(10.dp))
if (conversation.unreadCount > 0) {
Box(
modifier = Modifier
.size(if (conversation.unreadCount > 99) 24.dp else 20.dp)
.background(
color = AppColors.main,
color = Color(0xFFFF3B30),
shape = CircleShape
),
contentAlignment = Alignment.Center
) {
Text(
text = if (conversation.unreadCount > 99) "99+" else conversation.unreadCount.toString(),
color = AppColors.mainText,
fontSize = if (conversation.unreadCount > 99) 9.sp else 10.sp,
color = Color.White,
fontSize = if (conversation.unreadCount > 99) 11.sp else 12.sp,
fontWeight = FontWeight.Bold
)
}

View File

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

View File

@@ -17,7 +17,6 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
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.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@@ -146,8 +144,6 @@ fun NewsCommentModal(
var showCommentMenu by remember { mutableStateOf(false) }
var contextComment by remember { mutableStateOf<CommentEntity?>(null) }
var replyComment by remember { mutableStateOf<CommentEntity?>(null) }
val listState = rememberLazyListState()
val scope = rememberCoroutineScope()
// 菜单弹窗
if (showCommentMenu) {
@@ -230,14 +226,9 @@ fun NewsCommentModal(
.fillMaxWidth(),
horizontalArrangement = Arrangement.End
) {
OrderSelectionComponent(
selectedOrder = commentViewModel.order
) {
OrderSelectionComponent {
commentViewModel.order = it
commentViewModel.reloadComment()
scope.launch {
listState.scrollToItem(0)
}
}
}
}
@@ -276,9 +267,7 @@ fun NewsCommentModal(
)
}
} else {
LazyColumn(
state = listState
) {
LazyColumn {
item {
CommentContent(
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.sp
import com.aiosman.ravenow.R
import com.aiosman.ravenow.LocalNavController
import com.aiosman.ravenow.entity.MomentEntity
import com.aiosman.ravenow.ui.NavigationRoute
import com.aiosman.ravenow.ui.comment.CommentModalContent
import com.aiosman.ravenow.ui.composables.CustomAsyncImage
import com.aiosman.ravenow.ui.modifiers.noRippleClickable
import com.aiosman.ravenow.data.UserService
import com.aiosman.ravenow.data.UserServiceImpl
import kotlinx.coroutines.launch
import androidx.compose.runtime.rememberCoroutineScope
/**
* 动态推荐Item组件post_normal
@@ -50,16 +56,37 @@ fun PostRecommendationItem(
moment: MomentEntity,
onLikeClick: ((MomentEntity) -> Unit)? = null,
onCommentClick: ((MomentEntity) -> Unit)? = null,
onCommentAdded: ((MomentEntity) -> Unit)? = null,
onFavoriteClick: ((MomentEntity) -> Unit)? = null,
onShareClick: ((MomentEntity) -> Unit)? = null,
modifier: Modifier = Modifier
) {
val context = LocalContext.current
val navController = LocalNavController.current
val scope = rememberCoroutineScope()
val userService: UserService = UserServiceImpl()
var showCommentModal by remember { mutableStateOf(false) }
var sheetState = rememberModalBottomSheetState(
skipPartiallyExpanded = true
)
// 导航到个人资料
fun navigateToProfile() {
scope.launch {
try {
val profile = userService.getUserProfile(moment.authorId.toString())
navController.navigate(
NavigationRoute.AccountProfile.route
.replace("{id}", profile.id.toString())
.replace("{isAiAccount}", if (profile.aiAccount) "true" else "false")
)
} catch (e: Exception) {
// 处理错误,避免崩溃
e.printStackTrace()
}
}
}
// 图片列表
val images = moment.images
val imageCount = images.size
@@ -71,8 +98,9 @@ fun PostRecommendationItem(
) {
// 图片显示区域(替代视频播放器)
if (imageCount > 0) {
// 只显示第一张图片,优先使用 thumbnailDirectUrl
val imageUrl = images[0].thumbnailDirectUrl
// 只显示第一张图片,优先使用 smallDirectUrl
val imageUrl = images[0].smallDirectUrl
?: images[0].thumbnailDirectUrl
?: images[0].directUrl
?: images[0].url
CustomAsyncImage(
@@ -104,32 +132,16 @@ fun PostRecommendationItem(
.fillMaxWidth()
.padding(start = 16.dp, bottom = 16.dp)
) {
// 用户头像和昵称
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.padding(bottom = 8.dp)
) {
Box(
modifier = Modifier
.size(40.dp)
.clip(CircleShape)
.background(Color.White.copy(alpha = 0.2f))
) {
CustomAsyncImage(
imageUrl = moment.avatar,
contentDescription = "用户头像",
modifier = Modifier.fillMaxSize(),
defaultRes = R.drawable.default_avatar
)
}
Text(
text = "@${moment.nickname}",
modifier = Modifier.padding(start = 8.dp),
fontSize = 16.sp,
color = Color.White,
style = TextStyle(fontWeight = FontWeight.Bold)
)
}
// 用户昵称
Text(
text = "@${moment.nickname}",
modifier = Modifier
.padding(bottom = 8.dp)
.noRippleClickable { navigateToProfile() },
fontSize = 16.sp,
color = Color.White,
style = TextStyle(fontWeight = FontWeight.Bold)
)
// 文字内容
if (!moment.momentTextContent.isNullOrEmpty()) {
@@ -160,7 +172,10 @@ fun PostRecommendationItem(
horizontalAlignment = Alignment.CenterHorizontally
) {
// 用户头像
UserAvatar(avatarUrl = moment.avatar)
UserAvatar(
avatarUrl = moment.avatar,
onClick = { navigateToProfile() }
)
// 点赞
VideoBtn(
@@ -205,21 +220,35 @@ fun PostRecommendationItem(
containerColor = Color.White,
sheetState = sheetState
) {
CommentModalContent(postId = moment.id) {
// 评论添加后的回调
}
CommentModalContent(
postId = moment.id,
commentCount = moment.commentCount,
onCommentAdded = {
onCommentAdded?.invoke(moment)
}
)
}
}
}
@Composable
private fun UserAvatar(avatarUrl: String? = null) {
private fun UserAvatar(
avatarUrl: String? = null,
onClick: (() -> Unit)? = null
) {
Box(
modifier = Modifier
.padding(bottom = 16.dp)
.size(40.dp)
.clip(CircleShape)
.background(Color.White.copy(alpha = 0.2f))
.then(
if (onClick != null) {
Modifier.noRippleClickable { onClick() }
} else {
Modifier
}
)
) {
if (avatarUrl != null && avatarUrl.isNotEmpty()) {
CustomAsyncImage(

View File

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

View File

@@ -16,7 +16,10 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
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.Icon
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.Text
import androidx.compose.material3.rememberModalBottomSheetState
@@ -30,11 +33,8 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalLifecycleOwner
@@ -97,10 +97,6 @@ fun VideoRecommendationItem(
skipPartiallyExpanded = true
)
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) {
ExoPlayer.Builder(context)
@@ -171,43 +167,29 @@ fun VideoRecommendationItem(
},
modifier = Modifier
.fillMaxSize()
.pointerInput(videoUrl, moment.liked) {
detectTapGestures(
onDoubleTap = { offset ->
// 双击点赞/取消点赞
val currentTime = System.currentTimeMillis()
if (currentTime - lastDoubleTapTime.value > doubleTapDebounceTime) {
lastDoubleTapTime.value = currentTime
// 检查当前点赞状态,如果已点赞则取消点赞,如果未点赞则点赞
onLikeClick?.invoke(moment)
}
},
onTap = {
// 单击播放/暂停
pauseIconVisibleState = true
.noRippleClickable {
pauseIconVisibleState = true
exoPlayer.pause()
scope.launch {
delay(100)
if (exoPlayer.isPlaying) {
exoPlayer.pause()
scope.launch {
delay(100)
if (exoPlayer.isPlaying) {
exoPlayer.pause()
} else {
pauseIconVisibleState = false
exoPlayer.play()
}
}
} else {
pauseIconVisibleState = false
exoPlayer.play()
}
)
}
}
)
if (pauseIconVisibleState) {
Image(
painter = painterResource(R.mipmap.dt_ts_sp_bf_btn),
Icon(
imageVector = Icons.Default.PlayArrow,
contentDescription = null,
modifier = Modifier
.align(Alignment.Center)
.size(80.dp),
colorFilter = ColorFilter.tint(Color.White)
tint = Color.White
)
}
}
@@ -318,9 +300,7 @@ fun VideoRecommendationItem(
ModalBottomSheet(
onDismissRequest = { showCommentModal = false },
containerColor = Color.White,
sheetState = sheetState,
dragHandle = {},
shape = RoundedCornerShape(topStart = 28.dp, topEnd = 28.dp)
sheetState = sheetState
) {
CommentModalContent(postId = moment.id) {
// 评论添加后的回调
@@ -340,18 +320,11 @@ fun VideoRecommendationItem(
val observer = LifecycleEventObserver { _, event ->
when (event) {
Lifecycle.Event.ON_PAUSE -> {
shouldResumeAfterLifecyclePause = exoPlayer.isPlaying && !pauseIconVisibleState
exoPlayer.pause()
}
Lifecycle.Event.ON_RESUME -> {
if (isVisible && shouldResumeAfterLifecyclePause) {
pauseIconVisibleState = false
if (isVisible) {
exoPlayer.play()
} else {
// 未自动恢复播放时,如果当前可见且视频已暂停,则显示暂停图标
if (isVisible && !exoPlayer.isPlaying) {
pauseIconVisibleState = true
}
}
}
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 com.aiosman.ravenow.utils.NetworkUtils
import androidx.compose.ui.platform.LocalContext
import com.aiosman.ravenow.ui.network.NetworkErrorContentInline
/**
* 动态列表
@@ -91,11 +90,38 @@ fun TimelineMomentsList() {
.padding(top = 188.dp),
contentAlignment = Alignment.TopCenter
) {
NetworkErrorContentInline(
onReload = {
model.refreshPager(pullRefresh = true)
}
)
Column(
horizontalAlignment = Alignment.CenterHorizontally,
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()) {
Box(

View File

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

View File

@@ -52,7 +52,6 @@ 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.draw.alpha
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Brush
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.NestedScrollSource
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.LocalDensity
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.GroupChatEmptyContent
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.UserAgentsRow
import com.aiosman.ravenow.ui.index.tabs.profile.composable.UserContentPageIndicator
@@ -173,41 +168,6 @@ fun ProfileV3(
initialFirstVisibleItemScrollOffset = model.profileGridFirstVisibleItemOffset
)
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) {
object : NestedScrollConnection {
override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
@@ -532,19 +492,10 @@ fun ProfileV3(
.background(AppColors.profileBackground)
.padding(top = 8.dp)
) {
Box(
modifier = Modifier
.onGloballyPositioned { coordinates ->
tabIndicatorHeightPx = coordinates.size.height
tabIndicatorContentOffset = coordinates.positionInRoot().y + scrollState.value
}
.alpha(if (shouldStickTabBar) 0f else 1f)
) {
UserContentPageIndicator(
pagerState = pagerState,
showAgentTab = !isAiAccount
)
}
UserContentPageIndicator(
pagerState = pagerState,
showAgentTab = !isAiAccount
)
HorizontalPager(
state = pagerState,
modifier = Modifier.height(650.dp) // 固定滚动高度
@@ -582,53 +533,22 @@ fun ProfileV3(
showNoMoreText = isSelf,
modifier = Modifier.fillMaxSize(),
state = listState,
nestedScrollConnection = nestedScrollConnection,
showSegments = isSelf, // 只有查看自己的主页时才显示分段控制器
segmentSelectedIndex = agentSegmentSelected,
onSegmentSelected = { agentSegmentSelected = it },
onSegmentMeasured = { offset, height ->
agentSegmentOffset = offset
agentSegmentHeightPx = height
},
isSegmentSticky = shouldStickAgentSegments,
parentScrollProvider = { scrollState.value }
nestedScrollConnection = nestedScrollConnection
)
} else {
// 查看其他用户的主页时传递该用户的会话ID以显示其创建的群聊查看自己的主页时传递null
GroupChatPlaceholder(
modifier = Modifier.fillMaxSize(),
listState = groupChatListState,
nestedScrollConnection = nestedScrollConnection,
ownerSessionId = externalOwnerSessionId,
showSegments = isSelf, // 只有查看自己的主页时才显示分段控制器
selectedSegmentIndex = groupSegmentSelected,
onSegmentSelected = { groupSegmentSelected = it },
onSegmentMeasured = { offset, height ->
groupSegmentOffset = offset
groupSegmentHeightPx = height
},
isSegmentSticky = shouldStickGroupSegments,
parentScrollProvider = { scrollState.value }
nestedScrollConnection = nestedScrollConnection
)
}
}
2 -> {
if (!isAiAccount) {
// 查看其他用户的主页时传递该用户的会话ID以显示其创建的群聊查看自己的主页时传递null
GroupChatPlaceholder(
modifier = Modifier.fillMaxSize(),
listState = groupChatListState,
nestedScrollConnection = nestedScrollConnection,
ownerSessionId = externalOwnerSessionId,
showSegments = isSelf, // 只有查看自己的主页时才显示分段控制器
selectedSegmentIndex = groupSegmentSelected,
onSegmentSelected = { groupSegmentSelected = it },
onSegmentMeasured = { offset, height ->
groupSegmentOffset = offset
groupSegmentHeightPx = height
},
isSegmentSticky = shouldStickGroupSegments,
parentScrollProvider = { scrollState.value }
nestedScrollConnection = nestedScrollConnection
)
}
}
@@ -640,55 +560,6 @@ fun ProfileV3(
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(
isMain = isMain,
@@ -785,26 +656,12 @@ fun ProfileV3(
private fun GroupChatPlaceholder(
modifier: Modifier = Modifier,
listState: androidx.compose.foundation.lazy.LazyListState,
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 }
nestedScrollConnection: NestedScrollConnection? = null
) {
GroupChatEmptyContent(
modifier = modifier,
listState = listState,
nestedScrollConnection = nestedScrollConnection,
ownerSessionId = ownerSessionId,
showSegments = showSegments,
selectedSegmentIndex = selectedSegmentIndex,
onSegmentSelected = onSegmentSelected,
onSegmentMeasured = onSegmentMeasured,
isSegmentSticky = isSegmentSticky,
parentScrollProvider = parentScrollProvider
nestedScrollConnection = nestedScrollConnection
)
}

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
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
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.fillMaxSize
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.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.LazyGridState
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.itemsIndexed
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
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.mutableStateOf
import androidx.compose.runtime.remember
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.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.TextOverflow
import com.aiosman.ravenow.AppState
@@ -158,8 +155,7 @@ fun GalleryGrid(
modifier = baseModifier
.fillMaxSize()
.verticalScroll(rememberScrollState())
.padding(vertical = 60.dp)
.padding(horizontal = 16.dp),
.padding(vertical = 60.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Image(
@@ -202,8 +198,24 @@ fun GalleryGrid(
.padding(vertical = 60.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
EmptyStateView(
contentDescription = "暂无图片"
Image(
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 {
@@ -215,20 +227,8 @@ fun GalleryGrid(
.padding(bottom = 8.dp),
) {
itemsIndexed(moments) { idx, moment ->
moment?.let { momentItem ->
if (moment != null && moment.images.isNotEmpty()) {
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(
modifier = Modifier
.fillMaxWidth()
@@ -237,32 +237,20 @@ fun GalleryGrid(
.noRippleClickable {
itemDebouncer {
navController.navigateToPost(
id = momentItem.id,
id = moment.id,
highlightCommentId = 0,
initImagePagerIndex = 0
)
}
}
) {
if (previewUrl != null) {
CustomAsyncImage(
imageUrl = previewUrl,
contentDescription = "",
modifier = Modifier.fillMaxSize(),
context = LocalContext.current
)
} 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) {
CustomAsyncImage(
imageUrl = moment.images[0].thumbnail,
contentDescription = "",
modifier = Modifier.fillMaxSize(),
context = LocalContext.current
)
if (moment.images.size > 1) {
Box(
modifier = Modifier
.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.lazy.LazyColumn
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.material.ExperimentalMaterialApi
import androidx.compose.material.pullrefresh.PullRefreshIndicator
import androidx.compose.material.pullrefresh.pullRefresh
import androidx.compose.material.pullrefresh.rememberPullRefreshState
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Text
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.draw.alpha
import com.airbnb.lottie.compose.LottieAnimation
import com.airbnb.lottie.compose.LottieCompositionSpec
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.graphics.Color
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.res.painterResource
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.data.api.ApiClient
import android.util.Base64
import com.aiosman.ravenow.ui.network.ReloadButton
import com.aiosman.ravenow.utils.NetworkUtils.isNetworkAvailable
@OptIn(ExperimentalMaterialApi::class)
@Composable
fun GroupChatEmptyContent(
modifier: Modifier = Modifier,
listState: LazyListState,
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 }
nestedScrollConnection: NestedScrollConnection? = null
) {
var selectedSegment by remember { mutableStateOf(0) } // 0: 全部, 1: 公开, 2: 私有
val AppColors = LocalAppTheme.current
val context = LocalContext.current
val navController = LocalNavController.current
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(
refreshing = if (canLoadRooms) viewModel.roomsRefreshing else false,
refreshing = viewModel.roomsRefreshing,
onRefresh = {
if (canLoadRooms) {
viewModel.refreshRooms(filterType = filterType, ownerSessionId = normalizedOwnerSessionId)
}
viewModel.refreshRooms(filterType = selectedSegment)
}
)
// 当分段或用户ID改变时,重新加载数据
LaunchedEffect(selectedSegmentIndex, normalizedOwnerSessionId, showSegments) {
if (canLoadRooms) {
viewModel.refreshRooms(filterType = filterType, ownerSessionId = normalizedOwnerSessionId)
// 当分段改变时,重新加载数据
LaunchedEffect(selectedSegment) {
// 切换分段时重新加载
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))
// 只在查看自己的房间时显示分段控制器
if (showSegments) {
SegmentedControl(
selectedIndex = selectedSegmentIndex,
onSegmentSelected = onSegmentSelected,
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))
}
// 分段控制器
SegmentedControl(
selectedIndex = selectedSegment,
onSegmentSelected = {
selectedSegment = it
// LaunchedEffect 会监听 selectedSegment 的变化并自动刷新
},
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(8.dp))
Box(
modifier = nestedScrollModifier
.fillMaxSize()
.pullRefresh(state)
) {
if (!canLoadRooms) {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
CircularProgressIndicator()
}
} else if (viewModel.rooms.isEmpty() && !viewModel.roomsLoading) {
if (viewModel.rooms.isEmpty() && !viewModel.roomsLoading) {
// 空状态内容(居中)
Column(
modifier = nestedScrollModifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally
) {
// 空状态插图
EmptyStateIllustration(
isNetworkAvailable = networkAvailable,
onReload = {
MyProfileViewModel.ResetModel()
MyProfileViewModel.loadProfile(pullRefresh = true)
}
)
EmptyStateIllustration()
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 {
LazyColumn(
state = listState,
modifier = nestedScrollModifier.fillMaxSize()
) {
// 网格布局每行显示2个房间卡片
items(
items = viewModel.rooms.chunked(2),
key = { rowRooms -> rowRooms.firstOrNull()?.id?.toString() ?: "" }
) { rowRooms ->
Row(
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 12.dp),
horizontalArrangement = Arrangement.spacedBy(12.dp)
) {
rowRooms.forEach { room ->
RoomCard(
room = room,
onRoomClick = { roomEntity ->
// 导航到群聊聊天界面
navController.navigateToGroupChat(
id = roomEntity.trtcRoomId,
name = roomEntity.name,
avatar = roomEntity.avatar
)
},
modifier = Modifier.weight(1f)
itemsIndexed(
items = viewModel.rooms,
key = { _, item -> item.id }
) { index, room ->
RoomItem(
room = room,
onRoomClick = { roomEntity ->
// 导航到群聊聊天界面
navController.navigateToGroupChat(
id = roomEntity.trtcRoomId,
name = roomEntity.name,
avatar = roomEntity.avatar
)
}
// 如果这一行只有一个房间,添加一个空的占位符
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,37 +201,30 @@ fun GroupChatEmptyContent(
if (viewModel.roomsHasMore && !viewModel.roomsLoading) {
item {
LaunchedEffect(Unit) {
viewModel.loadMoreRooms(
filterType = filterType,
ownerSessionId = normalizedOwnerSessionId
)
viewModel.loadMoreRooms(filterType = selectedSegment)
}
}
}
}
}
if (canLoadRooms) {
PullRefreshIndicator(
refreshing = viewModel.roomsRefreshing,
state = state,
modifier = Modifier.align(Alignment.TopCenter)
)
}
PullRefreshIndicator(
refreshing = viewModel.roomsRefreshing,
state = state,
modifier = Modifier.align(Alignment.TopCenter)
)
}
}
}
@Composable
fun RoomCard(
fun RoomItem(
room: RoomEntity,
onRoomClick: (RoomEntity) -> Unit = {},
modifier: Modifier = Modifier
onRoomClick: (RoomEntity) -> Unit = {}
) {
val AppColors = LocalAppTheme.current
val context = LocalContext.current
val roomDebouncer = rememberDebouncer()
val cardSize = 180.dp
// 构建头像URL
val avatarUrl = if (room.avatar.isNotEmpty()) {
@@ -276,117 +238,6 @@ fun RoomCard(
"${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
fun RoomItem(
room: RoomEntity,
onRoomClick: (RoomEntity) -> Unit = {}
) {
val AppColors = LocalAppTheme.current
val context = LocalContext.current
val roomDebouncer = rememberDebouncer()
// 构建头像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}"
}
Row(
modifier = Modifier
.fillMaxWidth()
@@ -407,7 +258,7 @@ fun RoomItem(
.clip(RoundedCornerShape(12.dp))
)
}
Column(
modifier = Modifier
.weight(1f)
@@ -427,9 +278,9 @@ fun RoomItem(
overflow = TextOverflow.Ellipsis
)
}
Spacer(modifier = Modifier.height(4.dp))
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically
@@ -442,9 +293,9 @@ fun RoomItem(
overflow = TextOverflow.Ellipsis,
modifier = Modifier.weight(1f)
)
Spacer(modifier = Modifier.width(8.dp))
Text(
text = "${room.userCount}",
fontSize = 12.sp,
@@ -456,7 +307,7 @@ fun RoomItem(
}
@Composable
fun SegmentedControl(
private fun SegmentedControl(
selectedIndex: Int,
onSegmentSelected: (Int) -> Unit,
modifier: Modifier = Modifier
@@ -521,7 +372,7 @@ private fun SegmentButton(
},
shape = RoundedCornerShape(1000.dp)
)
.noRippleClickable { onClick() },
.clickable(onClick = onClick),
contentAlignment = Alignment.Center
) {
Text(
@@ -538,43 +389,14 @@ private fun SegmentButton(
}
@Composable
private fun EmptyStateIllustration(
isNetworkAvailable: Boolean,
onReload: () -> Unit
) {
val AppColors = LocalAppTheme.current
if (isNetworkAvailable) {
EmptyStateView(
contentDescription = "空状态",
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)
}
private fun EmptyStateIllustration() {
Image(
painter = painterResource(id = R.mipmap.l_empty_img),
contentDescription = "空状态",
modifier = Modifier
.width(181.dp)
.height(153.dp),
contentScale = ContentScale.Fit
)
}

View File

@@ -29,12 +29,9 @@ 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.draw.alpha
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter
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.res.painterResource
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.ui.composables.CustomAsyncImage
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.utils.DebounceUtils
import com.aiosman.ravenow.utils.NetworkUtils
@@ -70,13 +66,7 @@ fun UserAgentsList(
showNoMoreText: Boolean = false,
modifier: Modifier = Modifier,
state: LazyListState,
nestedScrollConnection: NestedScrollConnection? = null,
showSegments: Boolean = true, // 是否显示分段控制器(全部、公开、私有)
segmentSelectedIndex: Int = 0,
onSegmentSelected: (Int) -> Unit = {},
onSegmentMeasured: ((Float, Int) -> Unit)? = null,
isSegmentSticky: Boolean = false,
parentScrollProvider: () -> Int = { 0 }
nestedScrollConnection: NestedScrollConnection? = null
) {
val AppColors = LocalAppTheme.current
val listModifier = if (nestedScrollConnection != null) {
@@ -90,14 +80,7 @@ fun UserAgentsList(
Box(
modifier = listModifier.fillMaxSize()
) {
AgentEmptyContentWithSegments(
showSegments = showSegments,
segmentSelectedIndex = segmentSelectedIndex,
onSegmentSelected = onSegmentSelected,
onSegmentMeasured = onSegmentMeasured,
isSegmentSticky = isSegmentSticky,
parentScrollProvider = parentScrollProvider
)
AgentEmptyContentWithSegments()
}
} else {
LazyColumn(
@@ -265,14 +248,8 @@ fun UserAgentCard(
}
@Composable
fun AgentEmptyContentWithSegments(
showSegments: Boolean = true,
segmentSelectedIndex: Int = 0,
onSegmentSelected: (Int) -> Unit = {},
onSegmentMeasured: ((Float, Int) -> Unit)? = null,
isSegmentSticky: Boolean = false,
parentScrollProvider: () -> Int = { 0 }
) {
fun AgentEmptyContentWithSegments() {
var selectedSegment by remember { mutableStateOf(0) } // 0: 全部, 1: 公开, 2: 私有
val AppColors = LocalAppTheme.current
val isNetworkAvailable = NetworkUtils.isNetworkAvailable(LocalContext.current)
@@ -283,24 +260,14 @@ fun AgentEmptyContentWithSegments(
) {
Spacer(modifier = Modifier.height(16.dp))
// 只在查看自己的智能体时显示分段控制器
if (showSegments) {
AgentSegmentedControl(
selectedIndex = segmentSelectedIndex,
onSegmentSelected = onSegmentSelected,
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))
}
// 分段控制器
AgentSegmentedControl(
selectedIndex = selectedSegment,
onSegmentSelected = { selectedSegment = it },
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(8.dp))
// 空状态内容(与动态、群聊保持一致)
Column(
@@ -308,8 +275,25 @@ fun AgentEmptyContentWithSegments(
horizontalAlignment = Alignment.CenterHorizontally
) {
if (isNetworkAvailable) {
EmptyStateView(
contentDescription = "暂无Agent"
Image(
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 {
Image(
@@ -348,7 +332,7 @@ fun AgentEmptyContentWithSegments(
}
@Composable
fun AgentSegmentedControl(
private fun AgentSegmentedControl(
selectedIndex: Int,
onSegmentSelected: (Int) -> Unit,
modifier: Modifier = Modifier
@@ -413,7 +397,7 @@ private fun AgentSegmentButton(
},
shape = RoundedCornerShape(1000.dp)
)
.noRippleClickable { onClick() },
.clickable(onClick = onClick),
contentAlignment = Alignment.Center
) {
Text(

View File

@@ -84,7 +84,6 @@ import com.aiosman.ravenow.ui.NavigationRoute
import com.google.accompanist.systemuicontroller.rememberSystemUiController
import kotlinx.coroutines.launch
import com.aiosman.ravenow.utils.NetworkUtils
import com.aiosman.ravenow.ui.network.NetworkErrorContentInline
@OptIn(ExperimentalFoundationApi::class)
@@ -416,17 +415,64 @@ fun MomentResultTab() {
.background(AppColors.background)
) {
if (moments.itemCount == 0 && model.showResult) {
val isNetworkAvailable = NetworkUtils.isNetworkAvailable(context)
SearchPlaceholderContent(
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp),
isNetworkAvailable = isNetworkAvailable,
onReload = {
SearchViewModel.ResetModel()
SearchViewModel.search()
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
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 {
LazyColumn(
modifier = Modifier.fillMaxSize(),
@@ -520,17 +566,64 @@ fun UserResultTab() {
modifier = Modifier.fillMaxSize()
) {
if (users.itemCount == 0 && model.showResult) {
val isNetworkAvailable = NetworkUtils.isNetworkAvailable(context)
SearchPlaceholderContent(
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp),
isNetworkAvailable = isNetworkAvailable,
onReload = {
SearchViewModel.ResetModel()
SearchViewModel.search()
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
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 {
LazyColumn(
modifier = Modifier.fillMaxSize(),
@@ -641,17 +734,64 @@ fun AiResultTab() {
.background(AppColors.background)
) {
if (agents.itemCount == 0 && model.showResult) {
val isNetworkAvailable = NetworkUtils.isNetworkAvailable(context)
SearchPlaceholderContent(
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp),
isNetworkAvailable = isNetworkAvailable,
onReload = {
SearchViewModel.ResetModel()
SearchViewModel.search()
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
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 {
LazyColumn(
modifier = Modifier.fillMaxSize(),
@@ -723,17 +863,65 @@ fun RoomResultTab() {
.background(AppColors.background)
) {
if (rooms.itemCount == 0 && model.showResult) {
val isNetworkAvailable = NetworkUtils.isNetworkAvailable(context)
SearchPlaceholderContent(
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp),
isNetworkAvailable = isNetworkAvailable,
onReload = {
SearchViewModel.ResetModel()
SearchViewModel.search()
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
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 {
LazyColumn(
modifier = Modifier.fillMaxSize(),
@@ -757,32 +945,41 @@ fun RoomResultTab() {
}
@Composable
fun SearchPlaceholderContent(
modifier: Modifier = Modifier,
isNetworkAvailable: Boolean,
onReload: () -> Unit
fun ReloadButton(
onClick: () -> Unit
) {
val appColors = LocalAppTheme.current
Column(
modifier = modifier,
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
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)
) {
if (isNetworkAvailable) {
androidx.compose.foundation.Image(
painter = painterResource(id = R.mipmap.empty_img),
contentDescription = "No Comment",
modifier = Modifier.size(168.dp)
)
Box(
modifier = Modifier
.fillMaxSize()
.background(gradientBrush),
contentAlignment = Alignment.Center
) {
Text(
text = stringResource(R.string.null_search),
color = appColors.text,
text = stringResource(R.string.Reload),
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.MomentRemoteDataSource
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.AgentRemoteDataSource
import com.aiosman.ravenow.entity.AgentSearchPagingSource
@@ -33,7 +31,6 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import org.greenrobot.eventbus.EventBus
object SearchViewModel : ViewModel() {
var searchText by mutableStateOf("")
@@ -207,14 +204,7 @@ object SearchViewModel : ViewModel() {
suspend fun likeMoment(id: Int) {
try {
momentService.likeMoment(id)
val likeCount = updateMomentLike(id, true)
EventBus.getDefault().post(
MomentLikeChangeEvent(
postId = id,
likeCount = likeCount,
isLike = true
)
)
updateMomentLike(id, true)
} catch (e: Exception) {
e.printStackTrace()
}
@@ -223,14 +213,7 @@ object SearchViewModel : ViewModel() {
suspend fun dislikeMoment(id: Int) {
try {
momentService.dislikeMoment(id)
val likeCount = updateMomentLike(id, false)
EventBus.getDefault().post(
MomentLikeChangeEvent(
postId = id,
likeCount = likeCount,
isLike = false
)
)
updateMomentLike(id, false)
} catch (e: Exception) {
e.printStackTrace()
}
@@ -240,12 +223,6 @@ object SearchViewModel : ViewModel() {
try {
momentService.favoriteMoment(id)
updateMomentFavorite(id, true)
EventBus.getDefault().post(
MomentFavouriteChangeEvent(
postId = id,
isFavourite = true
)
)
} catch (e: Exception) {
e.printStackTrace()
}
@@ -255,12 +232,6 @@ object SearchViewModel : ViewModel() {
try {
momentService.unfavoriteMoment(id)
updateMomentFavorite(id, false)
EventBus.getDefault().post(
MomentFavouriteChangeEvent(
postId = id,
isFavourite = false
)
)
} catch (e: Exception) {
e.printStackTrace()
}
@@ -270,23 +241,19 @@ object SearchViewModel : ViewModel() {
updateMomentCommentCount(id, 1)
}
private fun updateMomentLike(id: Int, isLike: Boolean): Int? {
var latestLikeCount: Int? = null
private fun updateMomentLike(id: Int, isLike: Boolean) {
val currentPagingData = _momentsFlow.value
val updatedPagingData = currentPagingData.map { momentItem ->
if (momentItem.id == id) {
val nextCount = (momentItem.likeCount + if (isLike) 1 else -1).coerceAtLeast(0)
latestLikeCount = nextCount
momentItem.copy(
liked = isLike,
likeCount = nextCount
likeCount = momentItem.likeCount + if (isLike) 1 else -1
)
} else {
momentItem
}
}
_momentsFlow.value = updatedPagingData
return latestLikeCount
}
private fun updateMomentFavorite(id: Int, isFavorite: Boolean) {
@@ -295,7 +262,7 @@ object SearchViewModel : ViewModel() {
if (momentItem.id == id) {
momentItem.copy(
isFavorite = isFavorite,
favoriteCount = (momentItem.favoriteCount + if (isFavorite) 1 else -1).coerceAtLeast(0)
favoriteCount = momentItem.favoriteCount + if (isFavorite) 1 else -1
)
} else {
momentItem

View File

@@ -13,7 +13,6 @@ import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
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.shape.RoundedCornerShape
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.Icon
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.Text
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.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.viewinterop.AndroidView
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
@@ -318,9 +319,9 @@ private fun SingleVideoItemContent(
isPageVisible: Boolean = true
) {
// 将暂停状态移到每个视频项内部,使用 remember 保存,避免在点赞/关注时被重置
val pauseIconVisibleState = remember(pager) { mutableStateOf(false) }
// 记录进入后台前是否在播放,用于决定是否需要自动恢复播放
val shouldResumeAfterLifecyclePause = remember(pager) { mutableStateOf(false) }
val pauseIconVisibleState = remember(pager) {
mutableStateOf(false)
}
// 当页面切换时,重置暂停状态
LaunchedEffect(pager, pagerState.currentPage) {
@@ -340,7 +341,6 @@ private fun SingleVideoItemContent(
pagerState = pagerState,
pager = pager,
pauseIconVisibleState = pauseIconVisibleState,
shouldResumeAfterLifecyclePause = shouldResumeAfterLifecyclePause,
onLikeClick = onLikeClick,
onCommentClick = onCommentClick,
onCommentAdded = onCommentAdded,
@@ -373,7 +373,6 @@ fun VideoPlayer(
pagerState: PagerState,
pager: Int,
pauseIconVisibleState: MutableState<Boolean>,
shouldResumeAfterLifecyclePause: MutableState<Boolean>,
onLikeClick: ((MomentEntity) -> Unit)? = null,
onCommentClick: ((MomentEntity) -> Unit)? = null,
onCommentAdded: ((MomentEntity) -> Unit)? = null,
@@ -468,9 +467,6 @@ fun VideoPlayer(
.clip(RectangleShape)
) {
var playerView by remember { mutableStateOf<PlayerView?>(null) }
// 防抖:记录上次双击时间,防止快速重复双击
val lastDoubleTapTime = remember { mutableStateOf(0L) }
val doubleTapDebounceTime = 500L // 500ms 防抖时间
// 使用 key 强制每个视频的 PlayerView 完全独立,避免布局状态残留
androidx.compose.runtime.key(videoUrl) {
@@ -483,31 +479,15 @@ fun VideoPlayer(
modifier = Modifier
.fillMaxSize()
.clip(RectangleShape)
.pointerInput(videoUrl, moment?.liked) {
detectTapGestures(
onDoubleTap = { offset ->
// 双击点赞/取消点赞
val currentTime = System.currentTimeMillis()
if (currentTime - lastDoubleTapTime.value > doubleTapDebounceTime) {
lastDoubleTapTime.value = currentTime
moment?.let {
// 检查当前点赞状态,如果已点赞则取消点赞,如果未点赞则点赞
onLikeClick?.invoke(it)
}
}
},
onTap = {
// 单击播放/暂停
handleVideoClick(pauseIconVisibleState, exoPlayer, scope)
}
)
.noRippleClickable {
handleVideoClick(pauseIconVisibleState, exoPlayer, scope)
}
)
}
if (pauseIconVisibleState.value) {
Image(
painter = painterResource(R.mipmap.dt_ts_sp_bf_btn),
Icon(
imageVector = Icons.Default.PlayArrow,
contentDescription = null,
modifier = Modifier
.align(Alignment.Center)
@@ -534,26 +514,15 @@ fun VideoPlayer(
when (event) {
Lifecycle.Event.ON_PAUSE -> {
// 应用进入后台时暂停
shouldResumeAfterLifecyclePause.value = exoPlayer.isPlaying && !pauseIconVisibleState.value
exoPlayer.playWhenReady = false
exoPlayer.pause()
}
Lifecycle.Event.ON_RESUME -> {
// 返回前台且为当前页面时恢复播放
if (
pager == pagerState.currentPage &&
isPageVisible &&
shouldResumeAfterLifecyclePause.value
) {
if (pager == pagerState.currentPage) {
exoPlayer.playWhenReady = true
exoPlayer.play()
pauseIconVisibleState.value = false
} else {
// 未自动恢复播放时,如果当前页面视频处于暂停状态,则显示暂停图标
if (!exoPlayer.isPlaying) {
pauseIconVisibleState.value = true
}
}
}
@@ -691,8 +660,7 @@ fun VideoPlayer(
},
containerColor = AppColors.background,
sheetState = sheetState,
dragHandle = {},
shape = RoundedCornerShape(topStart = 28.dp, topEnd = 28.dp)
shape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp)
) {
Box(
modifier = Modifier
@@ -702,7 +670,6 @@ fun VideoPlayer(
CommentModalContent(
postId = moment.id,
commentCount = moment.commentCount,
showTitle = false,
onCommentAdded = {
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.stringResource
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.unit.dp
import androidx.compose.ui.unit.sp
@@ -47,10 +46,10 @@ import com.aiosman.ravenow.ui.modifiers.noRippleClickable
import com.aiosman.ravenow.ui.navigateToPost
import java.util.Date
import com.aiosman.ravenow.utils.NetworkUtils
import com.aiosman.ravenow.ui.network.NetworkErrorContent
import com.aiosman.ravenow.ui.network.ReloadButton
@Preview
@Composable
fun LikeNoticeScreen(includeStatusBarPadding: Boolean = true) {
fun LikeNoticeScreen() {
val model = LikeNoticeViewModel
val listState = rememberLazyListState()
var dataFlow = model.likeItemsFlow
@@ -64,8 +63,7 @@ fun LikeNoticeScreen(includeStatusBarPadding: Boolean = true) {
StatusBarMaskLayout(
darkIcons = !AppState.darkMode,
maskBoxBackgroundColor = AppColors.background,
includeStatusBarPadding = includeStatusBarPadding
maskBoxBackgroundColor = AppColors.background
) {
Column(
modifier = Modifier
@@ -77,11 +75,42 @@ fun LikeNoticeScreen(includeStatusBarPadding: Boolean = true) {
val isNetworkAvailable = NetworkUtils.isNetworkAvailable(LocalContext.current)
if (!isNetworkAvailable) {
NetworkErrorContent(
onReload = {
LikeNoticeViewModel.reload(force = true)
Box(
modifier = Modifier.fillMaxSize()
.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) {
Box(
modifier = Modifier.fillMaxSize()
@@ -155,83 +184,57 @@ fun ActionPostNoticeItem(
val navController = LocalNavController.current
val AppColors = LocalAppTheme.current
val actionLabel = when (action) {
"favourite" -> stringResource(R.string.favourite_your_post)
else -> stringResource(R.string.like_your_post)
}
Row(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 12.dp, horizontal = 0.dp),
verticalAlignment = Alignment.CenterVertically
Box(
modifier = Modifier.padding(vertical = 16.dp)
) {
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(
modifier = Modifier
.weight(1f)
.padding(start = 8.dp)
.noRippleClickable {
navController.navigateToPost(
id = postId,
highlightCommentId = 0,
initImagePagerIndex = 0
)
},
verticalAlignment = Alignment.CenterVertically
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.Top,
) {
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(
modifier = Modifier.weight(1f)
modifier = Modifier
.weight(1f)
.noRippleClickable {
navController.navigateToPost(
id = postId,
highlightCommentId = 0,
initImagePagerIndex = 0
)
}
) {
Text(
text = nickName,
fontWeight = FontWeight.Bold,
fontSize = 14.sp,
color = AppColors.text,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
Spacer(modifier = Modifier.height(4.dp))
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
)
Text(nickName, fontWeight = FontWeight.Bold, fontSize = 16.sp, color = AppColors.text)
Spacer(modifier = Modifier.height(2.dp))
when (action) {
"like" -> Text(stringResource(R.string.like_your_post), color = AppColors.text)
"favourite" -> Text(stringResource(R.string.favourite_your_post), color = AppColors.text)
}
Spacer(modifier = Modifier.height(2.dp))
Row {
Text(likeTime.timeAgo(context), fontSize = 12.sp, color = AppColors.secondaryText)
}
}
Spacer(modifier = Modifier.width(4.dp))
CustomAsyncImage(
context = context,
context,
imageUrl = thumbnail,
modifier = Modifier
.size(40.dp)
.size(48.dp)
.clip(RoundedCornerShape(8.dp)),
contentDescription = action,
)
@@ -246,11 +249,10 @@ fun LikeCommentNoticeItem(
val navController = LocalNavController.current
val context = LocalContext.current
val AppColors = LocalAppTheme.current
val previewPost = item.comment?.replyComment?.post ?: item.comment?.post
Column(
Box(
modifier = Modifier
.padding(vertical = 12.dp)
.padding(vertical = 16.dp)
.noRippleClickable {
item.comment?.postId.let {
navController.navigateToPost(
@@ -261,103 +263,105 @@ fun LikeCommentNoticeItem(
}
}
) {
Row(
verticalAlignment = Alignment.CenterVertically
) {
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
Row {
Column(
modifier = Modifier.weight(1f)
) {
Column(
modifier = Modifier.weight(1f)
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.Top,
) {
Text(
text = item.user.nickName,
fontWeight = FontWeight.Bold,
fontSize = 14.sp,
color = AppColors.text,
maxLines = 1,
overflow = TextOverflow.Ellipsis
CustomAsyncImage(
imageUrl = item.user.avatar,
modifier = Modifier
.size(48.dp)
.clip(CircleShape),
contentDescription = stringResource(R.string.like_your_comment)
)
Spacer(modifier = Modifier.height(4.dp))
Row(
verticalAlignment = Alignment.CenterVertically
Spacer(modifier = Modifier.width(12.dp))
Column(
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 = stringResource(R.string.like_your_comment),
text = AppState.profile?.nickName ?: "",
fontWeight = FontWeight.W600,
fontSize = 14.sp,
color = AppColors.secondaryText,
modifier = Modifier.weight(1f),
maxLines = 1,
overflow = TextOverflow.Ellipsis
color = AppColors.text
)
Spacer(modifier = Modifier.width(4.dp))
Text(
text = item.likeTime.timeAgo(context),
fontSize = 14.sp,
color = AppColors.secondaryText
text = item.comment?.content ?: "",
fontSize = 12.sp,
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(
context = context,
imageUrl = it.images[0].thumbnail,
contentDescription = "Post Thumbnail",
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)),
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.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
@@ -90,7 +89,7 @@ fun NotificationScreen() {
modifier = Modifier
.fillMaxWidth()
.wrapContentHeight()
.padding(start = 16.dp, top = 8.dp),
.padding(start = 16.dp, top = 8.dp, bottom = 16.dp),
horizontalArrangement = Arrangement.Start,
verticalAlignment = Alignment.Bottom
) {
@@ -107,7 +106,7 @@ fun NotificationScreen() {
TabSpacer()
TabItem(
text = stringResource(R.string.follow_upper),
text = stringResource(R.string.followers_upper),
isSelected = pagerState.currentPage == 1,
onClick = {
scope.launch {
@@ -136,9 +135,9 @@ fun NotificationScreen() {
.weight(1f)
) { page ->
when (page) {
0 -> LikeNoticeScreen(includeStatusBarPadding = false)
1 -> FollowerNoticeScreen(includeStatusBarPadding = false)
2 -> CommentNoticeScreen(includeStatusBarPadding = false)
0 -> LikeNoticeScreen()
1 -> FollowerNoticeScreen()
2 -> CommentNoticeScreen()
}
}
}

View File

@@ -23,10 +23,12 @@ import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.fillMaxHeight
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.ExperimentalMaterial3Api
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.rememberModalBottomSheetState
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.Color
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
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(
onDismissRequest = onClose, // 允许通过代码关闭(如返回按钮)
sheetState = sheetState,
containerColor = Color.Transparent,
dragHandle = null, // 移除拖动手柄
shape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp)
containerColor = AppColors.background,
dragHandle = null // 移除拖动手柄
) {
Box(
modifier = Modifier
.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
.fillMaxWidth()
.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(
modifier = Modifier
@@ -341,7 +340,6 @@ fun PointsBottomSheet(
} else {
HowToEarnList(onRecharge = onRecharge)
}
}
}
}
}

View File

@@ -13,30 +13,37 @@ import kotlinx.coroutines.launch
class CommentsViewModel(
var postId: String = 0.toString(),
) : ViewModel() {
companion object {
private const val ORDER_ALL = "all"
private const val COMMENTS_PAGE_SIZE = 50
}
var commentService: CommentService = CommentServiceImpl()
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 subCommentLoadingMap by mutableStateOf(mutableMapOf<Int, Boolean>())
var highlightCommentId by mutableStateOf<Int?>(null)
var highlightComment by mutableStateOf<CommentEntity?>(null)
var isLoading 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 之前设置好内容
*/
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() {
viewModelScope.launch {
loadComments(page = 1, reset = true)
}
}
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) {
try {
isLoading = true
val response = commentService.getComments(
pageNumber = 1,
postId = postId.toInt(),
order = order,
pageSize = 50
)
commentsList = response.list
hasError = false
} else {
isLoadingMore = true
}
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) {
} catch (e: Exception) {
e.printStackTrace()
hasError = true
commentsList = emptyList()
}
} finally {
if (reset) {
} finally {
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) {
highlightCommentId = commentId

View File

@@ -22,7 +22,6 @@ import androidx.compose.material3.Divider
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.rememberModalBottomSheetState
import androidx.compose.runtime.Composable
@@ -35,7 +34,6 @@ import android.graphics.BitmapFactory
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
@@ -80,108 +78,92 @@ fun DraftBoxBottomSheet(
ModalBottomSheet(
onDismissRequest = onDismiss,
sheetState = sheetState,
containerColor = Color.Transparent,
containerColor = AppColors.background,
shape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp),
dragHandle = {}
) {
Box(
Column(
modifier = Modifier
.fillMaxWidth()
.padding(top = 8.dp)
.fillMaxHeight(0.9f)
.padding(horizontal = 16.dp, vertical = 8.dp)
) {
Surface(
// 标题
Box(
modifier = Modifier
.fillMaxWidth()
.fillMaxHeight()
.clip(RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp)),
color = AppColors.background,
tonalElevation = 0.dp,
shadowElevation = 0.dp,
.padding(vertical = 16.dp),
contentAlignment = Alignment.Center
) {
Column(
Text(
text = stringResource(R.string.drafts),
fontSize = 17.sp,
fontWeight = FontWeight.Bold,
color = AppColors.text
)
}
// 草稿列表
if (drafts.isEmpty()) {
Box(
modifier = Modifier
.fillMaxWidth()
.fillMaxHeight()
.padding(horizontal = 16.dp, vertical = 8.dp)
.fillMaxHeight(),
contentAlignment = Alignment.Center
) {
// 标题
Box(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 16.dp),
contentAlignment = Alignment.Center
) {
Text(
text = stringResource(R.string.drafts),
fontSize = 17.sp,
fontWeight = FontWeight.Bold,
color = AppColors.text
)
}
// 草稿列表
if (drafts.isEmpty()) {
Box(
modifier = Modifier
.fillMaxWidth()
.fillMaxHeight(),
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))
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()
}
}
}
// 底部提示
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
},
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))
}
}
}
// 底部提示
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
import android.Manifest
import android.content.pm.PackageManager
import android.net.Uri
import android.widget.Toast
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.content.ContextCompat
import androidx.compose.animation.core.LinearEasing
import androidx.compose.animation.core.RepeatMode
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 canAddMoreImages = model.imageList.size < 9
@@ -673,27 +642,14 @@ fun AddImageGrid() {
.background(Color(0xFFFAF9FB))
.noRippleClickable {
if (model.imageList.size < 9) {
// 检查摄像头权限
when {
ContextCompat.checkSelfPermission(
context,
Manifest.permission.CAMERA
) == PackageManager.PERMISSION_GRANTED -> {
// 已有权限,直接打开相机
val photoFile = File(context.cacheDir, "photo.jpg")
val photoUri: Uri = FileProvider.getUriForFile(
context,
"${context.packageName}.fileprovider",
photoFile
)
model.currentPhotoUri = photoUri
takePictureLauncher.launch(photoUri)
}
else -> {
// 没有权限,请求权限
requestCameraPermissionLauncher.launch(Manifest.permission.CAMERA)
}
}
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()
}

View File

@@ -12,8 +12,6 @@ import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutVertically
import androidx.compose.foundation.ExperimentalFoundationApi
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.combinedClickable
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.asPaddingValues
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
@@ -47,7 +44,6 @@ import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable
@@ -304,35 +300,20 @@ fun PostScreen(
onDismissRequest = {
showReportDialog = false
},
containerColor = Color.Transparent,
containerColor = AppColors.background,
sheetState = rememberModalBottomSheetState(
skipPartiallyExpanded = true
),
dragHandle = {},
shape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp),
) {
Box(
modifier = Modifier
.fillMaxWidth()
.padding(top = 7.dp)
) {
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
}
)
ReportModal(
momentId = viewModel.moment!!.id,
onClose = {
showReportDialog = false
}
}
)
}
}
Scaffold(
@@ -512,9 +493,7 @@ fun PostScreen(
color = AppColors.nonActiveText
)
Spacer(modifier = Modifier.weight(1f))
OrderSelectionComponent(
selectedOrder = commentsViewModel.order
) {
OrderSelectionComponent() {
commentsViewModel.order = it
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) {
Box(
@@ -1207,26 +1159,14 @@ fun ImageViewerDialog(
}
}
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun PostImageView(
images: List<MomentImageEntity>,
initialPage: Int? = 0
) {
val context = LocalContext.current
val scope = rememberCoroutineScope()
var isImageViewerDialog by remember { mutableStateOf(false) }
val initialPageIndex = 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
}
var currentImageIndex by remember { mutableStateOf(initialPage ?: 0) }
DisposableEffect(Unit) {
onDispose {
@@ -1247,31 +1187,23 @@ fun PostImageView(
modifier = Modifier
) {
if (images.isNotEmpty()) {
HorizontalPager(
state = pagerState,
CustomAsyncImage(
context,
images[currentImageIndex].thumbnail,
contentDescription = "Image",
contentScale = ContentScale.Crop,
modifier = Modifier
.weight(1f)
.fillMaxWidth()
) { page ->
val image = images[page]
CustomAsyncImage(
context,
image.thumbnail,
contentDescription = "Image",
blurHash = image.blurHash,
contentScale = ContentScale.Crop,
modifier = Modifier
.fillMaxSize()
.pointerInput(Unit) {
detectTapGestures(
onTap = {
isImageViewerDialog = true
}
)
}
.background(Color.Gray.copy(alpha = 0.1f))
)
}
.pointerInput(Unit) {
detectTapGestures(
onTap = {
isImageViewerDialog = true
}
)
}
.background(Color.Gray.copy(alpha = 0.1f))
)
}
// 图片导航控件
@@ -1280,26 +1212,56 @@ fun PostImageView(
modifier = Modifier
.padding(8.dp)
.fillMaxWidth(),
horizontalArrangement = Arrangement.Center,
horizontalArrangement = Arrangement.SpaceBetween,
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
images.forEachIndexed { index, _ ->
Box(
modifier = Modifier
.size(4.dp)
.clip(CircleShape)
.background(
if (pagerState.currentPage == index) Color.Red else Color.Gray.copy(
alpha = 0.5f
Row(
horizontalArrangement = Arrangement.Center
) {
images.forEachIndexed { index, _ ->
Box(
modifier = Modifier
.size(4.dp)
.clip(CircleShape)
.background(
if (currentImageIndex == index) Color.Red else Color.Gray.copy(
alpha = 0.5f
)
)
)
.padding(4.dp)
)
if (index < images.size - 1) {
Spacer(modifier = Modifier.width(8.dp))
.padding(4.dp)
)
if (index < images.size - 1) {
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
fun OrderSelectionComponent(
selectedOrder: String,
onSelected: (String) -> Unit = {}
) {
val AppColors = LocalAppTheme.current
var selectedOrder by remember { mutableStateOf("like") }
val orders = listOf(
"all" to stringResource(R.string.order_comment_default),
"latest" to stringResource(R.string.order_comment_latest),
"like" to stringResource(R.string.order_comment_hot)
"like" to stringResource(R.string.order_comment_default),
"earliest" to stringResource(R.string.order_comment_earliest),
"latest" to stringResource(R.string.order_comment_latest)
)
Box(
modifier = Modifier
@@ -1981,9 +1943,8 @@ fun OrderSelectionComponent(
Box(
modifier = Modifier
.noRippleClickable {
if (selectedOrder != order.first) {
onSelected(order.first)
}
selectedOrder = order.first
onSelected(order.first)
}
.background(
if (
@@ -2050,8 +2011,8 @@ fun ReportModal(
Column(
modifier = Modifier
.fillMaxWidth()
.fillMaxHeight()
.padding(start = 24.dp, end = 24.dp)
.background(AppColors.background)
.padding(start = 24.dp, end = 24.dp, bottom = 64.dp)
) {
Box(
modifier = Modifier.fillMaxWidth().padding(top = 16.dp, bottom = 16.dp),

View File

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

View File

@@ -1,6 +1,7 @@
package com.aiosman.ravenow.utils
import com.aiosman.ravenow.AppState
import com.aiosman.ravenow.entity.OpenIMMessageType
import com.google.gson.Gson
import com.google.gson.JsonSyntaxException
import io.openim.android.sdk.models.Message
@@ -25,8 +26,21 @@ object MessageParser {
val gson = Gson()
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)
@@ -50,6 +64,19 @@ object MessageParser {
* @return 消息的显示文本
*/
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) {
101 -> { // TEXT
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